From 1502c4c9c8737501708b0c7662e041a8a4cd6a8f Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Sat, 11 Nov 2017 21:17:00 +0100 Subject: [PATCH 1/3] Add getGenres app call. Signed-off-by: Yahor Berdnikau --- .../api/subsonic/SubsonicApiGetGenresTest.kt | 37 +++++++++++++++++++ .../resources/get_genres_ok.json | 29 +++++++++++++++ .../api/subsonic/SubsonicAPIDefinition.kt | 4 ++ .../ultrasonic/api/subsonic/models/Genre.kt | 7 ++++ .../api/subsonic/response/GenresResponse.kt | 15 ++++++++ 5 files changed, 92 insertions(+) create mode 100644 subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetGenresTest.kt create mode 100644 subsonic-api/src/integrationTest/resources/get_genres_ok.json create mode 100644 subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/Genre.kt create mode 100644 subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/GenresResponse.kt diff --git a/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetGenresTest.kt b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetGenresTest.kt new file mode 100644 index 00000000..1f311f07 --- /dev/null +++ b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetGenresTest.kt @@ -0,0 +1,37 @@ +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.Genre + +/** + * Integration test for [SubsonicAPIDefinition.getGenres] call. + */ +class SubsonicApiGetGenresTest : SubsonicAPIClientTest() { + @Test + fun `Should handle error response`() { + val response = checkErrorCallParsed(mockWebServerRule) { + client.api.getGenres().execute() + } + + response.genresList `should equal` emptyList() + } + + @Test + fun `Should handle ok response`() { + mockWebServerRule.enqueueResponse("get_genres_ok.json") + + val response = client.api.getGenres().execute() + + assertResponseSuccessful(response) + with(response.body().genresList) { + size `should equal to` 5 + this[0] `should equal` Genre(1186, 103, "Rock") + this[1] `should equal` Genre(896, 72, "Electronic") + this[2] `should equal` Genre(790, 59, "Alternative Rock") + this[3] `should equal` Genre(622, 97, "Trance") + this[4] `should equal` Genre(476, 36, "Hard Rock") + } + } +} diff --git a/subsonic-api/src/integrationTest/resources/get_genres_ok.json b/subsonic-api/src/integrationTest/resources/get_genres_ok.json new file mode 100644 index 00000000..a51ada16 --- /dev/null +++ b/subsonic-api/src/integrationTest/resources/get_genres_ok.json @@ -0,0 +1,29 @@ +{ + "subsonic-response" : { + "status" : "ok", + "version" : "1.15.0", + "genres" : { + "genre" : [ { + "songCount" : 1186, + "albumCount" : 103, + "value" : "Rock" + }, { + "songCount" : 896, + "albumCount" : 72, + "value" : "Electronic" + }, { + "songCount" : 790, + "albumCount" : 59, + "value" : "Alternative Rock" + }, { + "songCount" : 622, + "albumCount" : 97, + "value" : "Trance" + }, { + "songCount" : 476, + "albumCount" : 36, + "value" : "Hard Rock" + } ] + } + } +} \ 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 afce5798..fde41c0a 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 @@ -3,6 +3,7 @@ package org.moire.ultrasonic.api.subsonic import okhttp3.ResponseBody import org.moire.ultrasonic.api.subsonic.models.AlbumListType import org.moire.ultrasonic.api.subsonic.models.JukeboxAction +import org.moire.ultrasonic.api.subsonic.response.GenresResponse import org.moire.ultrasonic.api.subsonic.response.GetAlbumList2Response import org.moire.ultrasonic.api.subsonic.response.GetAlbumListResponse import org.moire.ultrasonic.api.subsonic.response.GetAlbumResponse @@ -197,4 +198,7 @@ interface SubsonicAPIDefinition { fun createShare(@Query("id") idsToShare: List, @Query("description") description: String? = null, @Query("expires") expires: Long? = null): Call + + @GET("getGenres.view") + fun getGenres(): Call } diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/Genre.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/Genre.kt new file mode 100644 index 00000000..6a54d43c --- /dev/null +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/Genre.kt @@ -0,0 +1,7 @@ +package org.moire.ultrasonic.api.subsonic.models + +import com.fasterxml.jackson.annotation.JsonProperty + +data class Genre(val songCount: Int = 0, + val albumCount: Int = 0, + @JsonProperty("value") val name: String) diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/GenresResponse.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/GenresResponse.kt new file mode 100644 index 00000000..fdf34c18 --- /dev/null +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/GenresResponse.kt @@ -0,0 +1,15 @@ +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.Genre + +class GenresResponse(status: Status, + version: SubsonicAPIVersions, + error: SubsonicError?) : SubsonicResponse(status, version, error) { + @JsonProperty("genres") private val genresWrapper = GenresWrapper() + val genresList: List get() = genresWrapper.genresList +} + +internal class GenresWrapper(@JsonProperty("genre") val genresList: List = emptyList()) From d693971bafd7f55a88098ee71df084beb1778505 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Sat, 11 Nov 2017 23:57:55 +0100 Subject: [PATCH 2/3] Add helper functions to convert api Genre to domain entity. Signed-off-by: Yahor Berdnikau --- .../org/moire/ultrasonic/domain/Genre.java | 20 +++++++++- .../ultrasonic/data/APIGenreConverter.kt | 13 +++++++ .../ultrasonic/data/ApiGenreConverterTest.kt | 38 +++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/APIGenreConverter.kt create mode 100644 ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/ApiGenreConverterTest.kt diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/domain/Genre.java b/ultrasonic/src/main/java/org/moire/ultrasonic/domain/Genre.java index 8ab360f3..d7cae20a 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/domain/Genre.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/domain/Genre.java @@ -31,7 +31,25 @@ public class Genre implements Serializable this.index = index; } - @Override + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Genre genre = (Genre) o; + + if (name != null ? !name.equals(genre.name) : genre.name != null) return false; + return index != null ? index.equals(genre.index) : genre.index == null; + } + + @Override + public int hashCode() { + int result = name != null ? name.hashCode() : 0; + result = 31 * result + (index != null ? index.hashCode() : 0); + return result; + } + + @Override public String toString() { return name; diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/APIGenreConverter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/APIGenreConverter.kt new file mode 100644 index 00000000..78607ec3 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/APIGenreConverter.kt @@ -0,0 +1,13 @@ +// Collection of functions to convert api Genre entity to domain entity +@file:JvmName("ApiGenreConverter") +package org.moire.ultrasonic.data + +import org.moire.ultrasonic.domain.Genre +import org.moire.ultrasonic.api.subsonic.models.Genre as APIGenre + +fun APIGenre.toDomainEntity(): Genre = Genre().apply { + name = this@toDomainEntity.name + index = this@toDomainEntity.name.substring(0, 1) +} + +fun List.toDomainEntityList(): List = this.map { it.toDomainEntity() } diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/ApiGenreConverterTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/ApiGenreConverterTest.kt new file mode 100644 index 00000000..bf7dc9eb --- /dev/null +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/ApiGenreConverterTest.kt @@ -0,0 +1,38 @@ +@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.Genre + +/** + * Unit test for for converter from api [Genre] to domain entity. + */ +class ApiGenreConverterTest { + @Test + fun `Should convert to domain entity`() { + val entity = Genre(songCount = 220, albumCount = 123, name = "some-name") + + val domainEntity = entity.toDomainEntity() + + with(domainEntity) { + name `should equal to` entity.name + index `should equal to` "s" + } + } + + @Test + fun `Should convert a list entites to domain entities`() { + val entitiesList = listOf( + Genre(41, 2, "some-name"), + Genre(12, 3, "other-name")) + + val domainEntitiesList = entitiesList.toDomainEntityList() + + domainEntitiesList.size `should equal to` entitiesList.size + domainEntitiesList[0] `should equal` entitiesList[0].toDomainEntity() + domainEntitiesList[1] `should equal` entitiesList[1].toDomainEntity() + } +} From 579accb06c7c03344c34281ca1e21fde090770a7 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Sat, 11 Nov 2017 23:59:00 +0100 Subject: [PATCH 3/3] Use new api getGenres call. Signed-off-by: Yahor Berdnikau --- .../ultrasonic/service/RESTMusicService.java | 25 ++- .../service/parser/GenreParser.java | 154 ------------------ 2 files changed, 10 insertions(+), 169 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/service/parser/GenreParser.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 cd1e809e..fecd9875 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java @@ -58,6 +58,7 @@ import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient; import org.moire.ultrasonic.api.subsonic.models.AlbumListType; import org.moire.ultrasonic.api.subsonic.models.JukeboxAction; import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild; +import org.moire.ultrasonic.api.subsonic.response.GenresResponse; import org.moire.ultrasonic.api.subsonic.response.GetAlbumList2Response; import org.moire.ultrasonic.api.subsonic.response.GetAlbumListResponse; import org.moire.ultrasonic.api.subsonic.response.GetAlbumResponse; @@ -92,6 +93,7 @@ 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.data.ApiGenreConverter; import org.moire.ultrasonic.domain.Bookmark; import org.moire.ultrasonic.domain.ChatMessage; import org.moire.ultrasonic.domain.Genre; @@ -110,7 +112,6 @@ import org.moire.ultrasonic.domain.Version; import org.moire.ultrasonic.service.parser.BookmarkParser; import org.moire.ultrasonic.service.parser.ChatMessageParser; import org.moire.ultrasonic.service.parser.ErrorParser; -import org.moire.ultrasonic.service.parser.GenreParser; import org.moire.ultrasonic.service.parser.MusicDirectoryParser; import org.moire.ultrasonic.service.parser.RandomSongsParser; import org.moire.ultrasonic.service.parser.SubsonicRESTException; @@ -1196,21 +1197,15 @@ public class RESTMusicService implements MusicService return networkInfo == null ? -1 : networkInfo.getType(); } - @Override - public List getGenres(Context context, ProgressListener progressListener) throws Exception - { - checkServerVersion(context, "1.9", "Genres not supported."); + @Override + public List getGenres(Context context, + ProgressListener progressListener) throws Exception { + updateProgressListener(progressListener, R.string.parser_reading); + Response response = subsonicAPIClient.getApi().getGenres().execute(); + checkResponseSuccessful(response); - Reader reader = getReader(context, progressListener, "getGenres", null); - try - { - return new GenreParser(context).parse(reader, progressListener); - } - finally - { - Util.close(reader); - } - } + return ApiGenreConverter.toDomainEntityList(response.body().getGenresList()); + } @Override public MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context, ProgressListener progressListener) throws Exception diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/parser/GenreParser.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/parser/GenreParser.java deleted file mode 100644 index 85104ae4..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/parser/GenreParser.java +++ /dev/null @@ -1,154 +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 2010 (C) Sindre Mehus - */ -package org.moire.ultrasonic.service.parser; - -import android.content.Context; -import android.util.Log; - -import org.moire.ultrasonic.R; -import org.moire.ultrasonic.domain.Genre; -import org.moire.ultrasonic.util.ProgressListener; - -import org.xmlpull.v1.XmlPullParser; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.Reader; -import java.io.StringReader; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Pattern; - -/** - * @author Joshua Bahnsen - */ -public class GenreParser extends AbstractParser -{ - - private static final String TAG = GenreParser.class.getSimpleName(); - private static final Pattern COMPILE = Pattern.compile("(?:&)(amp;|lt;|gt;|#37;|apos;)"); - private static final Pattern PATTERN = Pattern.compile("&(?!amp;|lt;|gt;|#37;|apos;)"); - private static final Pattern COMPILE1 = Pattern.compile("%"); - private static final Pattern COMPILE2 = Pattern.compile("'"); - - public GenreParser(Context context) - { - super(context); - } - - public List parse(Reader reader, ProgressListener progressListener) throws Exception - { - updateProgress(progressListener, R.string.parser_reading); - - List result = new ArrayList(); - StringReader sr = null; - - try - { - BufferedReader br = new BufferedReader(reader); - String xml = null; - String line; - - while ((line = br.readLine()) != null) - { - if (xml == null) - { - xml = line; - } - else - { - xml += line; - } - } - br.close(); - - // Replace possible unescaped XML characters - // No replacements for <> at this time - if (xml != null) - { - // Replace double escaped ampersand (&apos;) - xml = COMPILE.matcher(xml).replaceAll("&$1"); - - // Replace unescaped ampersand - xml = PATTERN.matcher(xml).replaceAll("&"); - - // Replace unescaped percent symbol - xml = COMPILE1.matcher(xml).replaceAll("%"); - - // Replace unescaped apostrophe - xml = COMPILE2.matcher(xml).replaceAll("'"); - } - - sr = new StringReader(xml); - } - catch (IOException ioe) - { - Log.e(TAG, "Error parsing Genre XML", ioe); - } - - if (sr == null) - { - Log.w(TAG, "Unable to parse Genre XML, returning empty list"); - return result; - } - - init(sr); - - Genre genre = null; - - int eventType; - do - { - eventType = nextParseEvent(); - if (eventType == XmlPullParser.START_TAG) - { - String name = getElementName(); - if ("genre".equals(name)) - { - genre = new Genre(); - } - else if ("error".equals(name)) - { - handleError(); - } - else - { - genre = null; - } - } - else if (eventType == XmlPullParser.TEXT) - { - if (genre != null) - { - String value = getText(); - - genre.setName(value); - genre.setIndex(value.substring(0, 1)); - result.add(genre); - genre = null; - } - } - } while (eventType != XmlPullParser.END_DOCUMENT); - - validate(); - updateProgress(progressListener, R.string.parser_reading_done); - - return result; - } -}