From b73cc0f816e985ef9df2d9e5c7b22d6282862a7c Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Sun, 27 Aug 2017 22:12:23 +0200 Subject: [PATCH 1/3] Add getPlaylist call to subsonic api. Signed-off-by: Yahor Berdnikau --- .../subsonic/SubsonicApiGetPlaylistTest.kt | 57 ++++++++++++++++ .../resources/get_playlist_ok.json | 66 +++++++++++++++++++ .../api/subsonic/SubsonicAPIDefinition.kt | 6 +- .../api/subsonic/models/Playlist.kt | 17 +++++ .../subsonic/response/GetPlaylistResponse.kt | 11 ++++ 5 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetPlaylistTest.kt create mode 100644 subsonic-api/src/integrationTest/resources/get_playlist_ok.json create mode 100644 subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/Playlist.kt create mode 100644 subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/GetPlaylistResponse.kt diff --git a/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetPlaylistTest.kt b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetPlaylistTest.kt new file mode 100644 index 00000000..add5ac05 --- /dev/null +++ b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicApiGetPlaylistTest.kt @@ -0,0 +1,57 @@ +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 [SubsonicAPIClient] for getPlaylist call. + */ +class SubsonicApiGetPlaylistTest : SubsonicAPIClientTest() { + @Test + fun `Should parse error response`() { + checkErrorCallParsed(mockWebServerRule) { + client.api.getPlaylist(10).execute() + } + } + + @Test + fun `Should parse ok response`() { + mockWebServerRule.enqueueResponse("get_playlist_ok.json") + + val response = client.api.getPlaylist(4).execute() + + assertResponseSuccessful(response) + with(response.body().playlist) { + id `should equal to` 0 + name `should equal to` "Aug 27, 2017 11:17 AM" + owner `should equal to` "admin" + public `should equal to` false + songCount `should equal to` 16 + duration `should equal to` 3573 + created `should equal` parseDate("2017-08-27T11:17:26.216Z") + changed `should equal` parseDate("2017-08-27T11:17:26.218Z") + coverArt `should equal to` "pl-0" + entriesList.size `should equal to` 2 + entriesList[1] `should equal` MusicDirectoryChild(id = 4215, parent = 4186, + isDir = false, title = "Going to Hell", album = "Going to Hell", + artist = "The Pretty Reckless", track = 2, year = 2014, + genre = "Hard Rock", coverArt = "4186", size = 11089627, + contentType = "audio/mpeg", suffix = "mp3", duration = 277, bitRate = 320, + path = "The Pretty Reckless/Going to Hell/02 Going to Hell.mp3", + isVideo = false, playCount = 0, discNumber = 1, + created = parseDate("2016-10-23T21:30:41.000Z"), + albumId = 388, artistId = 238, type = "music") + } + } + + @Test + fun `Should pass id as request param`() { + val playlistId = 453L + mockWebServerRule.assertRequestParam(responseResourceName = "get_playlist_ok.json", + apiRequest = { + client.api.getPlaylist(playlistId).execute() + }, expectedParam = "id=$playlistId") + } +} diff --git a/subsonic-api/src/integrationTest/resources/get_playlist_ok.json b/subsonic-api/src/integrationTest/resources/get_playlist_ok.json new file mode 100644 index 00000000..b7127519 --- /dev/null +++ b/subsonic-api/src/integrationTest/resources/get_playlist_ok.json @@ -0,0 +1,66 @@ +{ + "subsonic-response" : { + "status" : "ok", + "version" : "1.15.0", + "playlist" : { + "id" : "0", + "name" : "Aug 27, 2017 11:17 AM", + "owner" : "admin", + "public" : false, + "songCount" : 16, + "duration" : 3573, + "created" : "2017-08-27T11:17:26.216Z", + "changed" : "2017-08-27T11:17:26.218Z", + "coverArt" : "pl-0", + "entry" : [ { + "id" : "4209", + "parent" : "4186", + "isDir" : false, + "title" : "Follow Me Down", + "album" : "Going to Hell", + "artist" : "The Pretty Reckless", + "track" : 1, + "year" : 2014, + "genre" : "Hard Rock", + "coverArt" : "4186", + "size" : 11229681, + "contentType" : "audio/mpeg", + "suffix" : "mp3", + "duration" : 280, + "bitRate" : 320, + "path" : "The Pretty Reckless/Going to Hell/01 Follow Me Down.mp3", + "isVideo" : false, + "playCount" : 1, + "discNumber" : 1, + "created" : "2016-10-23T21:30:43.000Z", + "albumId" : "388", + "artistId" : "238", + "type" : "music" + }, { + "id" : "4215", + "parent" : "4186", + "isDir" : false, + "title" : "Going to Hell", + "album" : "Going to Hell", + "artist" : "The Pretty Reckless", + "track" : 2, + "year" : 2014, + "genre" : "Hard Rock", + "coverArt" : "4186", + "size" : 11089627, + "contentType" : "audio/mpeg", + "suffix" : "mp3", + "duration" : 277, + "bitRate" : 320, + "path" : "The Pretty Reckless/Going to Hell/02 Going to Hell.mp3", + "isVideo" : false, + "playCount" : 0, + "discNumber" : 1, + "created" : "2016-10-23T21:30:41.000Z", + "albumId" : "388", + "artistId" : "238", + "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 e16dbb01..4c0d3fcb 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 @@ -5,11 +5,12 @@ import org.moire.ultrasonic.api.subsonic.response.GetArtistResponse import org.moire.ultrasonic.api.subsonic.response.GetArtistsResponse import org.moire.ultrasonic.api.subsonic.response.GetIndexesResponse import org.moire.ultrasonic.api.subsonic.response.GetMusicDirectoryResponse +import org.moire.ultrasonic.api.subsonic.response.GetPlaylistResponse 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.SearchThreeResponse +import org.moire.ultrasonic.api.subsonic.response.SearchTwoResponse import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse import retrofit2.Call import retrofit2.http.GET @@ -83,4 +84,7 @@ interface SubsonicAPIDefinition { @Query("albumOffset") albumOffset: Int? = null, @Query("songCount") songCount: Int? = null, @Query("musicFolderId") musicFolderId: Long? = null): Call + + @GET("getPlaylist.view") + fun getPlaylist(@Query("id") id: Long): Call } diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/Playlist.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/Playlist.kt new file mode 100644 index 00000000..fc6e372f --- /dev/null +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/Playlist.kt @@ -0,0 +1,17 @@ +package org.moire.ultrasonic.api.subsonic.models + +import com.fasterxml.jackson.annotation.JsonProperty +import java.util.Calendar + +data class Playlist( + val id: Long = -1, + val name: String = "", + val owner: String = "", + val public: Boolean = false, + val songCount: Int = 0, + val duration: Long = 0, + val created: Calendar? = null, + val changed: Calendar? = null, + val coverArt: String = "", + @JsonProperty("entry") val entriesList: List = emptyList() +) diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/GetPlaylistResponse.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/GetPlaylistResponse.kt new file mode 100644 index 00000000..522761e0 --- /dev/null +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/GetPlaylistResponse.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.Playlist + +class GetPlaylistResponse( + status: Status, + version: SubsonicAPIVersions, + error: SubsonicError?, + val playlist: Playlist = Playlist()) : SubsonicResponse(status, version, error) From b8b53dc81d73d6c8baba7ed7c34a965d1113ef0b Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Sun, 27 Aug 2017 22:30:52 +0200 Subject: [PATCH 2/3] Add converting Playlist to MusicDirectoryDomainEntity. Signed-off-by: Yahor Berdnikau --- .../ultrasonic/data/SubsonicAPIConverter.kt | 6 ++++++ .../moire/ultrasonic/data/APIConverterTest.kt | 18 ++++++++++++++++++ 2 files changed, 24 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 048b5412..1f424417 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/SubsonicAPIConverter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/SubsonicAPIConverter.kt @@ -16,6 +16,7 @@ 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.Playlist as APIPlaylist import org.moire.ultrasonic.api.subsonic.models.SearchResult as APISearchResult fun APIMusicFolder.toDomainEntity(): MusicFolder = MusicFolder(this.id.toString(), this.name) @@ -103,3 +104,8 @@ fun SearchThreeResult.toDomainEntity(): SearchResult = SearchResult( this.artistList.map { it.toDomainEntity() }, this.albumList.map { it.toDomainEntity() }, this.songList.map { it.toDomainEntity() }) + +fun APIPlaylist.toMusicDirectoryDomainEntity(): MusicDirectory = MusicDirectory().apply { + name = this@toMusicDirectoryDomainEntity.name + addAll(this@toMusicDirectoryDomainEntity.entriesList.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 4e95a95b..291662b0 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/APIConverterTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/data/APIConverterTest.kt @@ -13,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.Playlist import org.moire.ultrasonic.api.subsonic.models.SearchResult import org.moire.ultrasonic.api.subsonic.models.SearchThreeResult import org.moire.ultrasonic.api.subsonic.models.SearchTwoResult @@ -257,6 +258,23 @@ class APIConverterTest { } } + @Test + fun `Should convert Playlist to MusicDirectory domain entity`() { + val entity = Playlist(name = "some-playlist-name", entriesList = listOf( + MusicDirectoryChild(id = 10L, parent = 1393), + MusicDirectoryChild(id = 11L, parent = 1393) + )) + + val convertedEntity = entity.toMusicDirectoryDomainEntity() + + with(convertedEntity) { + name `should equal to` entity.name + children.size `should equal to` entity.entriesList.size + children[0] `should equal` entity.entriesList[0].toDomainEntity() + children[1] `should equal` entity.entriesList[1].toDomainEntity() + } + } + private fun createMusicFolder(id: Long = 0, name: String = ""): MusicFolder = MusicFolder(id, name) From de0b57f9b820dbcd4757f0e7abb6cbf5e67040e9 Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Sun, 27 Aug 2017 22:36:37 +0200 Subject: [PATCH 3/3] Use new subsonic api getPlaylist() call. Signed-off-by: Yahor Berdnikau --- .../ultrasonic/service/RESTMusicService.java | 89 +++++++++---------- .../service/parser/PlaylistParser.java | 72 --------------- 2 files changed, 44 insertions(+), 117 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/service/parser/PlaylistParser.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 9a7d4ea7..e471bfde 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java @@ -61,6 +61,7 @@ import org.moire.ultrasonic.api.subsonic.response.GetArtistResponse; import org.moire.ultrasonic.api.subsonic.response.GetArtistsResponse; import org.moire.ultrasonic.api.subsonic.response.GetIndexesResponse; import org.moire.ultrasonic.api.subsonic.response.GetMusicDirectoryResponse; +import org.moire.ultrasonic.api.subsonic.response.GetPlaylistResponse; import org.moire.ultrasonic.api.subsonic.response.LicenseResponse; import org.moire.ultrasonic.api.subsonic.response.MusicFoldersResponse; import org.moire.ultrasonic.api.subsonic.response.SearchResponse; @@ -91,7 +92,6 @@ import org.moire.ultrasonic.service.parser.GenreParser; import org.moire.ultrasonic.service.parser.JukeboxStatusParser; import org.moire.ultrasonic.service.parser.LyricsParser; import org.moire.ultrasonic.service.parser.MusicDirectoryParser; -import org.moire.ultrasonic.service.parser.PlaylistParser; import org.moire.ultrasonic.service.parser.PlaylistsParser; import org.moire.ultrasonic.service.parser.PodcastEpisodeParser; import org.moire.ultrasonic.service.parser.PodcastsChannelsParser; @@ -473,54 +473,53 @@ public class RESTMusicService implements MusicService return APIConverter.toDomainEntity(response.body().getSearchResult()); } - @Override - public MusicDirectory getPlaylist(String id, String name, Context context, ProgressListener progressListener) throws Exception - { - HttpParams params = new BasicHttpParams(); - HttpConnectionParams.setSoTimeout(params, SOCKET_READ_TIMEOUT_GET_PLAYLIST); + @Override + public MusicDirectory getPlaylist(String id, + String name, + Context context, + ProgressListener progressListener) throws Exception { + if (id == null) { + throw new IllegalArgumentException("id param is null!"); + } - Reader reader = getReader(context, progressListener, "getPlaylist", params, "id", id); - try - { - MusicDirectory playlist = new PlaylistParser(context).parse(reader, progressListener); + updateProgressListener(progressListener, R.string.parser_reading); + Response response = subsonicAPIClient.getApi() + .getPlaylist(Long.valueOf(id)).execute(); + checkResponseSuccessful(response); - File playlistFile = FileUtil.getPlaylistFile(Util.getServerName(context), name); - FileWriter fw = new FileWriter(playlistFile); - BufferedWriter bw = new BufferedWriter(fw); - try - { - fw.write("#EXTM3U\n"); - for (MusicDirectory.Entry e : playlist.getChildren()) - { - String filePath = FileUtil.getSongFile(context, e).getAbsolutePath(); - if (!new File(filePath).exists()) - { - String ext = FileUtil.getExtension(filePath); - String base = FileUtil.getBaseName(filePath); - filePath = base + ".complete." + ext; - } - fw.write(filePath + '\n'); - } - } - catch (Exception e) - { - Log.w(TAG, "Failed to save playlist: " + name); - } - finally - { - bw.close(); - fw.close(); - } + MusicDirectory playlist = APIConverter + .toMusicDirectoryDomainEntity(response.body().getPlaylist()); + savePlaylist(name, context, playlist); + return playlist; + } - return playlist; - } - finally - { - Util.close(reader); - } - } + private void savePlaylist(String name, + Context context, + MusicDirectory playlist) throws IOException { + File playlistFile = FileUtil.getPlaylistFile(Util.getServerName(context), name); + FileWriter fw = new FileWriter(playlistFile); + BufferedWriter bw = new BufferedWriter(fw); + try { + fw.write("#EXTM3U\n"); + for (MusicDirectory.Entry e : playlist.getChildren()) { + String filePath = FileUtil.getSongFile(context, e).getAbsolutePath(); + if (!new File(filePath).exists()) { + String ext = FileUtil.getExtension(filePath); + String base = FileUtil.getBaseName(filePath); + filePath = base + ".complete." + ext; + } + fw.write(filePath + '\n'); + } + } catch (IOException e) { + Log.w(TAG, "Failed to save playlist: " + name); + throw e; + } finally { + bw.close(); + fw.close(); + } + } - @Override + @Override public List getPodcastsChannels(boolean refresh, Context context, ProgressListener progressListener) throws Exception { Reader reader = getReader(context, progressListener, "getPodcasts", null,"includeEpisodes", "false"); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/parser/PlaylistParser.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/parser/PlaylistParser.java deleted file mode 100644 index 8f52f283..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/parser/PlaylistParser.java +++ /dev/null @@ -1,72 +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.util.ProgressListener; - -import org.xmlpull.v1.XmlPullParser; - -import java.io.Reader; - -/** - * @author Sindre Mehus - */ -public class PlaylistParser extends MusicDirectoryEntryParser -{ - - public PlaylistParser(Context context) - { - super(context); - } - - public MusicDirectory parse(Reader reader, ProgressListener progressListener) throws Exception - { - updateProgress(progressListener, R.string.parser_reading); - init(reader); - - MusicDirectory dir = new MusicDirectory(); - int eventType; - do - { - eventType = nextParseEvent(); - if (eventType == XmlPullParser.START_TAG) - { - String name = getElementName(); - if ("entry".equals(name)) - { - dir.addChild(parseEntry("", false, 0)); - } - else if ("error".equals(name)) - { - handleError(); - } - } - } while (eventType != XmlPullParser.END_DOCUMENT); - - validate(); - updateProgress(progressListener, R.string.parser_reading_done); - - return dir; - } - -} \ No newline at end of file