From 4e6df12f4ecd346be769efc868caeff7adc3e417 Mon Sep 17 00:00:00 2001 From: Nite Date: Thu, 15 Oct 2020 10:22:15 +0200 Subject: [PATCH] Moved minimumApiVersion detection to be executed before any first request Refactored RESTMusicService to Kotlin Refactored OfflineMusicService not to be a subclass of RESTMusicService Minor fixes --- .../ultrasonic/activity/DownloadActivity.java | 1 + .../ultrasonic/activity/MainActivity.java | 28 +- .../service/OfflineMusicService.java | 245 ++-- .../ultrasonic/service/RESTMusicService.java | 1093 ---------------- .../ultrasonic/activity/EditServerActivity.kt | 35 +- .../moire/ultrasonic/di/MusicServiceModule.kt | 6 +- .../service/ApiCallResponseChecker.kt | 66 + .../ultrasonic/service/RESTMusicService.kt | 1094 +++++++++++++++++ 8 files changed, 1341 insertions(+), 1227 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/ApiCallResponseChecker.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/DownloadActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/DownloadActivity.java index 22786cd3..fdd68673 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/DownloadActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/DownloadActivity.java @@ -1218,6 +1218,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi @Override protected void error(final Throwable error) { + Timber.e(error, "Exception has occurred in savePlaylistInBackground"); final String msg = String.format("%s %s", getResources().getString(R.string.download_playlist_error), getErrorMessage(error)); Util.toast(DownloadActivity.this, msg); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/MainActivity.java b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/MainActivity.java index 6948927a..f467bde0 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/activity/MainActivity.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/activity/MainActivity.java @@ -36,12 +36,9 @@ import org.moire.ultrasonic.R; import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.data.ServerSetting; import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport; -import org.moire.ultrasonic.service.MusicService; -import org.moire.ultrasonic.service.MusicServiceFactory; import org.moire.ultrasonic.util.Constants; import org.moire.ultrasonic.util.FileUtil; import org.moire.ultrasonic.util.MergeAdapter; -import org.moire.ultrasonic.util.TabActivityBackgroundTask; import org.moire.ultrasonic.util.Util; import java.util.Collections; @@ -153,10 +150,6 @@ public class MainActivity extends SubsonicTabActivity adapter.addView(videosTitle, false); adapter.addViews(Collections.singletonList(videosButton), true); - - if (Util.isNetworkConnected(this)) { - new PingTask(this, false).execute(); - } } list.setAdapter(adapter); @@ -250,7 +243,7 @@ public class MainActivity extends SubsonicTabActivity { final SharedPreferences.Editor editor = preferences.edit(); editor.putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, FileUtil.getDefaultMusicDirectory(this).getPath()); - editor.commit(); + editor.apply(); } } @@ -386,23 +379,4 @@ public class MainActivity extends SubsonicTabActivity currentSetting.getPassword(), currentSetting.getAllowSelfSignedCertificate(), currentSetting.getLdapSupport(), currentSetting.getMinimumApiVersion()); } - - /** - * Temporary task to make a ping to server to get it supported api version. - */ - private static class PingTask extends TabActivityBackgroundTask { - PingTask(SubsonicTabActivity activity, boolean changeProgress) { - super(activity, changeProgress); - } - - @Override - protected Void doInBackground() throws Throwable { - final MusicService service = MusicServiceFactory.getMusicService(getActivity()); - service.ping(getActivity(), null); - return null; - } - - @Override - protected void done(Void result) {} - } } \ No newline at end of file diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java index 9c8fcd82..873d9b63 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java @@ -21,12 +21,14 @@ package org.moire.ultrasonic.service; import android.content.Context; import android.graphics.Bitmap; import android.media.MediaMetadataRetriever; + +import kotlin.Pair; import timber.log.Timber; -import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient; -import org.moire.ultrasonic.cache.PermanentFileStorage; import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.domain.Artist; +import org.moire.ultrasonic.domain.Bookmark; +import org.moire.ultrasonic.domain.ChatMessage; import org.moire.ultrasonic.domain.Genre; import org.moire.ultrasonic.domain.Indexes; import org.moire.ultrasonic.domain.JukeboxStatus; @@ -34,10 +36,12 @@ import org.moire.ultrasonic.domain.Lyrics; import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.domain.MusicFolder; import org.moire.ultrasonic.domain.Playlist; +import org.moire.ultrasonic.domain.PodcastsChannel; import org.moire.ultrasonic.domain.SearchCriteria; import org.moire.ultrasonic.domain.SearchResult; import org.moire.ultrasonic.domain.Share; import org.moire.ultrasonic.domain.UserInfo; +import org.moire.ultrasonic.util.CancellableTask; import org.moire.ultrasonic.util.Constants; import org.moire.ultrasonic.util.FileUtil; import org.moire.ultrasonic.util.ProgressListener; @@ -48,6 +52,7 @@ import java.io.BufferedWriter; import java.io.File; import java.io.FileReader; import java.io.FileWriter; +import java.io.InputStream; import java.io.Reader; import java.util.ArrayList; import java.util.Collection; @@ -68,22 +73,11 @@ import static org.koin.java.KoinJavaComponent.inject; /** * @author Sindre Mehus */ -public class OfflineMusicService extends RESTMusicService +public class OfflineMusicService implements MusicService { private static final Pattern COMPILE = Pattern.compile(" "); - private Lazy activeServerProvider = inject(ActiveServerProvider.class); - public OfflineMusicService(SubsonicAPIClient subsonicAPIClient, PermanentFileStorage storage) { - super(subsonicAPIClient, storage); - } - - @Override - public boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception - { - return true; - } - @Override public Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception { @@ -150,7 +144,7 @@ public class OfflineMusicService extends RESTMusicService } @Override - public MusicDirectory getMusicDirectory(String id, String artistName, boolean refresh, Context context, ProgressListener progressListener) throws Exception + public MusicDirectory getMusicDirectory(String id, String artistName, boolean refresh, Context context, ProgressListener progressListener) { File dir = new File(id); MusicDirectory result = new MusicDirectory(); @@ -341,7 +335,7 @@ public class OfflineMusicService extends RESTMusicService } @Override - public Bitmap getAvatar(Context context, String username, int size, boolean saveToFile, boolean highQuality, ProgressListener progressListener) throws Exception + public Bitmap getAvatar(Context context, String username, int size, boolean saveToFile, boolean highQuality, ProgressListener progressListener) { try { @@ -355,7 +349,7 @@ public class OfflineMusicService extends RESTMusicService } @Override - public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, boolean saveToFile, boolean highQuality, ProgressListener progressListener) throws Exception + public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, boolean saveToFile, boolean highQuality, ProgressListener progressListener) { try { @@ -369,25 +363,7 @@ public class OfflineMusicService extends RESTMusicService } @Override - public void star(String id, String albumId, String artistId, Context context, ProgressListener progressListener) throws Exception - { - throw new OfflineException("Star not available in offline mode"); - } - - @Override - public void unstar(String id, String albumId, String artistId, Context context, ProgressListener progressListener) throws Exception - { - throw new OfflineException("UnStar not available in offline mode"); - } - - @Override - public List getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception - { - throw new OfflineException("Music folders not available in offline mode"); - } - - @Override - public SearchResult search(SearchCriteria criteria, Context context, ProgressListener progressListener) throws Exception + public SearchResult search(SearchCriteria criteria, Context context, ProgressListener progressListener) { List artists = new ArrayList(); List albums = new ArrayList(); @@ -531,7 +507,7 @@ public class OfflineMusicService extends RESTMusicService } @Override - public List getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception + public List getPlaylists(boolean refresh, Context context, ProgressListener progressListener) { List playlists = new ArrayList(); File root = FileUtil.getPlaylistDirectory(context); @@ -661,6 +637,45 @@ public class OfflineMusicService extends RESTMusicService } } + + @Override + public MusicDirectory getRandomSongs(int size, Context context, ProgressListener progressListener) + { + File root = FileUtil.getMusicDirectory(context); + List children = new LinkedList(); + listFilesRecursively(root, children); + MusicDirectory result = new MusicDirectory(); + + if (children.isEmpty()) + { + return result; + } + + Random random = new java.security.SecureRandom(); + for (int i = 0; i < size; i++) + { + File file = children.get(random.nextInt(children.size())); + result.addChild(createEntry(context, file, getName(file))); + } + + return result; + } + + private static void listFilesRecursively(File parent, List children) + { + for (File file : FileUtil.listMediaFiles(parent)) + { + if (file.isFile()) + { + children.add(file); + } + else + { + listFilesRecursively(file, children); + } + } + } + @Override public void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception { @@ -691,12 +706,6 @@ public class OfflineMusicService extends RESTMusicService throw new OfflineException("Album lists not available in offline mode"); } - @Override - public String getVideoUrl(Context context, String id, boolean useFlash) - { - return null; - } - @Override public JukeboxStatus updateJukeboxPlaylist(List ids, Context context, ProgressListener progressListener) throws Exception { @@ -739,29 +748,6 @@ public class OfflineMusicService extends RESTMusicService throw new OfflineException("Starred not available in offline mode"); } - @Override - public MusicDirectory getRandomSongs(int size, Context context, ProgressListener progressListener) throws Exception - { - File root = FileUtil.getMusicDirectory(context); - List children = new LinkedList(); - listFilesRecursively(root, children); - MusicDirectory result = new MusicDirectory(); - - if (children.isEmpty()) - { - return result; - } - - Random random = new java.security.SecureRandom(); - for (int i = 0; i < size; i++) - { - File file = children.get(random.nextInt(children.size())); - result.addChild(createEntry(context, file, getName(file))); - } - - return result; - } - @Override public MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context, ProgressListener progressListener) throws Exception { @@ -804,18 +790,121 @@ public class OfflineMusicService extends RESTMusicService throw new OfflineException("Updating shares not available in offline mode"); } - private static void listFilesRecursively(File parent, List children) + @Override + public void star(String id, String albumId, String artistId, Context context, ProgressListener progressListener) throws Exception { - for (File file : FileUtil.listMediaFiles(parent)) - { - if (file.isFile()) - { - children.add(file); - } - else - { - listFilesRecursively(file, children); - } - } + throw new OfflineException("Star not available in offline mode"); + } + + @Override + public void unstar(String id, String albumId, String artistId, Context context, ProgressListener progressListener) throws Exception + { + throw new OfflineException("UnStar not available in offline mode"); + } + @Override + public List getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception + { + throw new OfflineException("Music folders not available in offline mode"); + } + + @Override + public MusicDirectory getAlbumList2(String type, int size, int offset, Context context, ProgressListener progressListener) { + Timber.w("OfflineMusicService.getAlbumList2 was called but it isn't available"); + return null; + } + + @Override + public String getVideoUrl(Context context, String id, boolean useFlash) { + Timber.w("OfflineMusicService.getVideoUrl was called but it isn't available"); + return null; + } + + @Override + public List getChatMessages(Long since, Context context, ProgressListener progressListener) { + Timber.w("OfflineMusicService.getChatMessages was called but it isn't available"); + return null; + } + + @Override + public void addChatMessage(String message, Context context, ProgressListener progressListener) { + Timber.w("OfflineMusicService.addChatMessage was called but it isn't available"); + } + + @Override + public List getBookmarks(Context context, ProgressListener progressListener) { + Timber.w("OfflineMusicService.getBookmarks was called but it isn't available"); + return null; + } + + @Override + public void deleteBookmark(String id, Context context, ProgressListener progressListener) { + Timber.w("OfflineMusicService.deleteBookmark was called but it isn't available"); + } + + @Override + public void createBookmark(String id, int position, Context context, ProgressListener progressListener) { + Timber.w("OfflineMusicService.createBookmark was called but it isn't available"); + } + + @Override + public MusicDirectory getVideos(boolean refresh, Context context, ProgressListener progressListener) { + Timber.w("OfflineMusicService.getVideos was called but it isn't available"); + return null; + } + + @Override + public SearchResult getStarred2(Context context, ProgressListener progressListener) { + Timber.w("OfflineMusicService.getStarred2 was called but it isn't available"); + return null; + } + + @Override + public void ping(Context context, ProgressListener progressListener) { + } + + @Override + public boolean isLicenseValid(Context context, ProgressListener progressListener) { + return true; + } + + @Override + public Indexes getArtists(boolean refresh, Context context, ProgressListener progressListener) { + Timber.w("OfflineMusicService.getArtists was called but it isn't available"); + return null; + } + + @Override + public MusicDirectory getArtist(String id, String name, boolean refresh, Context context, ProgressListener progressListener) { + Timber.w("OfflineMusicService.getArtist was called but it isn't available"); + return null; + } + + @Override + public MusicDirectory getAlbum(String id, String name, boolean refresh, Context context, ProgressListener progressListener) { + Timber.w("OfflineMusicService.getAlbum was called but it isn't available"); + return null; + } + + @Override + public MusicDirectory getPodcastEpisodes(String podcastChannelId, Context context, ProgressListener progressListener) { + Timber.w("OfflineMusicService.getPodcastEpisodes was called but it isn't available"); + return null; + } + + @Override + public Pair getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, CancellableTask task) { + Timber.w("OfflineMusicService.getDownloadInputStream was called but it isn't available"); + return null; + } + + @Override + public void setRating(String id, int rating, Context context, ProgressListener progressListener) { + Timber.w("OfflineMusicService.setRating was called but it isn't available"); + } + + @Override + public List getPodcastsChannels(boolean refresh, Context context, ProgressListener progressListener) { + Timber.w("OfflineMusicService.getPodcastsChannels was called but it isn't available"); + return null; } } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java deleted file mode 100644 index 0c2ab55c..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/RESTMusicService.java +++ /dev/null @@ -1,1093 +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; - -import android.content.Context; -import android.graphics.Bitmap; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import android.text.TextUtils; -import timber.log.Timber; - -import org.moire.ultrasonic.R; -import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException; -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.BookmarksResponse; -import org.moire.ultrasonic.api.subsonic.response.ChatMessagesResponse; -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; -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.GetLyricsResponse; -import org.moire.ultrasonic.api.subsonic.response.GetMusicDirectoryResponse; -import org.moire.ultrasonic.api.subsonic.response.GetPlaylistResponse; -import org.moire.ultrasonic.api.subsonic.response.GetPlaylistsResponse; -import org.moire.ultrasonic.api.subsonic.response.GetPodcastsResponse; -import org.moire.ultrasonic.api.subsonic.response.GetRandomSongsResponse; -import org.moire.ultrasonic.api.subsonic.response.GetSongsByGenreResponse; -import org.moire.ultrasonic.api.subsonic.response.GetStarredResponse; -import org.moire.ultrasonic.api.subsonic.response.GetStarredTwoResponse; -import org.moire.ultrasonic.api.subsonic.response.GetUserResponse; -import org.moire.ultrasonic.api.subsonic.response.JukeboxResponse; -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.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.api.subsonic.response.VideosResponse; -import org.moire.ultrasonic.cache.PermanentFileStorage; -import org.moire.ultrasonic.cache.serializers.DomainSerializers; -import org.moire.ultrasonic.data.ActiveServerProvider; -import org.moire.ultrasonic.domain.APIAlbumConverter; -import org.moire.ultrasonic.domain.APIArtistConverter; -import org.moire.ultrasonic.domain.APIBookmarkConverter; -import org.moire.ultrasonic.domain.APIChatMessageConverter; -import org.moire.ultrasonic.domain.APIIndexesConverter; -import org.moire.ultrasonic.domain.APIJukeboxConverter; -import org.moire.ultrasonic.domain.APILyricsConverter; -import org.moire.ultrasonic.domain.APIMusicDirectoryConverter; -import org.moire.ultrasonic.domain.APIMusicFolderConverter; -import org.moire.ultrasonic.domain.APIPlaylistConverter; -import org.moire.ultrasonic.domain.APIPodcastConverter; -import org.moire.ultrasonic.domain.APISearchConverter; -import org.moire.ultrasonic.domain.APIShareConverter; -import org.moire.ultrasonic.domain.APIUserConverter; -import org.moire.ultrasonic.domain.ApiGenreConverter; -import org.moire.ultrasonic.domain.Bookmark; -import org.moire.ultrasonic.domain.ChatMessage; -import org.moire.ultrasonic.domain.Genre; -import org.moire.ultrasonic.domain.Indexes; -import org.moire.ultrasonic.domain.JukeboxStatus; -import org.moire.ultrasonic.domain.Lyrics; -import org.moire.ultrasonic.domain.MusicDirectory; -import org.moire.ultrasonic.domain.MusicFolder; -import org.moire.ultrasonic.domain.Playlist; -import org.moire.ultrasonic.domain.PodcastsChannel; -import org.moire.ultrasonic.domain.SearchCriteria; -import org.moire.ultrasonic.domain.SearchResult; -import org.moire.ultrasonic.domain.Share; -import org.moire.ultrasonic.domain.UserInfo; -import org.moire.ultrasonic.util.CancellableTask; -import org.moire.ultrasonic.util.FileUtil; -import org.moire.ultrasonic.util.ProgressListener; -import org.moire.ultrasonic.util.Util; - -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileOutputStream; -import java.io.FileWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import kotlin.Lazy; -import kotlin.Pair; -import retrofit2.Response; - -import static org.koin.java.KoinJavaComponent.inject; - -/** - * @author Sindre Mehus - */ -public class RESTMusicService implements MusicService { - private Lazy activeServerProvider = inject(ActiveServerProvider.class); - - private static final String MUSIC_FOLDER_STORAGE_NAME = "music_folder"; - private static final String INDEXES_STORAGE_NAME = "indexes"; - private static final String ARTISTS_STORAGE_NAME = "artists"; - - private final SubsonicAPIClient subsonicAPIClient; - private final PermanentFileStorage fileStorage; - - public RESTMusicService( - final SubsonicAPIClient subsonicAPIClient, - final PermanentFileStorage fileStorage - ) { - this.subsonicAPIClient = subsonicAPIClient; - this.fileStorage = fileStorage; - } - - @Override - public void ping(Context context, ProgressListener progressListener) throws Exception { - updateProgressListener(progressListener, R.string.service_connecting); - - if (activeServerProvider.getValue().getActiveServer().getMinimumApiVersion() == null) { - try { - final Response response = subsonicAPIClient.getApi().ping().execute(); - if (response != null && response.body() != null) { - String restApiVersion = response.body().getVersion().getRestApiVersion(); - Timber.i("Server minimum API version set to %s", restApiVersion); - activeServerProvider.getValue().setMinimumApiVersion(restApiVersion); - } - } catch (Exception ignored) { - // This Ping is only used to get the API Version, if it fails, that's no problem. - } - } - - // This Ping will be now executed with the correct API Version, so it shouldn't fail - final Response response = subsonicAPIClient.getApi().ping().execute(); - checkResponseSuccessful(response); - } - - @Override - public boolean isLicenseValid(Context context, ProgressListener progressListener) - throws Exception { - updateProgressListener(progressListener, R.string.service_connecting); - - final Response response = subsonicAPIClient.getApi().getLicense().execute(); - - checkResponseSuccessful(response); - return response.body().getLicense().getValid(); - } - - @Override - public List getMusicFolders(boolean refresh, - Context context, - ProgressListener progressListener) throws Exception { - List cachedMusicFolders = fileStorage.load(MUSIC_FOLDER_STORAGE_NAME, - DomainSerializers.getMusicFolderListSerializer()); - if (cachedMusicFolders != null && !refresh) { - return cachedMusicFolders; - } - - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi().getMusicFolders().execute(); - checkResponseSuccessful(response); - - List musicFolders = APIMusicFolderConverter - .toDomainEntityList(response.body().getMusicFolders()); - fileStorage.store(MUSIC_FOLDER_STORAGE_NAME, musicFolders, - DomainSerializers.getMusicFolderListSerializer()); - return musicFolders; - } - - @Override - public Indexes getIndexes(String musicFolderId, - boolean refresh, - Context context, - ProgressListener progressListener) throws Exception { - Indexes cachedIndexes = fileStorage.load(INDEXES_STORAGE_NAME, - DomainSerializers.getIndexesSerializer()); - if (cachedIndexes != null && !refresh) { - return cachedIndexes; - } - - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .getIndexes(musicFolderId, null).execute(); - checkResponseSuccessful(response); - - Indexes indexes = APIIndexesConverter.toDomainEntity(response.body().getIndexes()); - fileStorage.store(INDEXES_STORAGE_NAME, indexes, DomainSerializers.getIndexesSerializer()); - return indexes; - } - - @Override - public Indexes getArtists(boolean refresh, - Context context, - ProgressListener progressListener) throws Exception { - Indexes cachedArtists = fileStorage - .load(ARTISTS_STORAGE_NAME, DomainSerializers.getIndexesSerializer()); - if (cachedArtists != null && !refresh) { - return cachedArtists; - } - - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .getArtists(null).execute(); - checkResponseSuccessful(response); - - Indexes indexes = APIIndexesConverter.toDomainEntity(response.body().getIndexes()); - fileStorage.store(ARTISTS_STORAGE_NAME, indexes, DomainSerializers.getIndexesSerializer()); - return indexes; - } - - @Override - public void star(String id, - String albumId, - String artistId, - Context context, - ProgressListener progressListener) throws Exception { - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .star(id, albumId, artistId).execute(); - checkResponseSuccessful(response); - } - - @Override - public void unstar(String id, - String albumId, - String artistId, - Context context, - ProgressListener progressListener) throws Exception { - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .unstar(id, albumId, artistId).execute(); - checkResponseSuccessful(response); - } - - @Override - public void setRating(String id, - int rating, - Context context, - ProgressListener progressListener) throws Exception { - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .setRating(id, rating).execute(); - checkResponseSuccessful(response); - } - - @Override - public MusicDirectory getMusicDirectory(String id, - String name, - boolean refresh, - Context context, - ProgressListener progressListener) throws Exception { - if (id == null) { - throw new IllegalArgumentException("Id should not be null!"); - } - - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .getMusicDirectory(id).execute(); - checkResponseSuccessful(response); - - return APIMusicDirectoryConverter.toDomainEntity(response.body().getMusicDirectory()); - } - - @Override - public MusicDirectory getArtist(String id, - String name, - boolean refresh, - Context context, - ProgressListener progressListener) throws Exception { - if (id == null) { - throw new IllegalArgumentException("Id can't be null!"); - } - - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi().getArtist(id).execute(); - checkResponseSuccessful(response); - - return APIArtistConverter.toMusicDirectoryDomainEntity(response.body().getArtist()); - } - - @Override - public MusicDirectory getAlbum(String id, - String name, - boolean refresh, - Context context, - ProgressListener progressListener) throws Exception { - if (id == null) { - throw new IllegalArgumentException("Id argument is null!"); - } - - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi().getAlbum(id).execute(); - checkResponseSuccessful(response); - - return APIAlbumConverter.toMusicDirectoryDomainEntity(response.body().getAlbum()); - } - - @Override - public SearchResult search(SearchCriteria criteria, - Context context, - ProgressListener progressListener) throws Exception { - try { - return !ActiveServerProvider.Companion.isOffline(context) && - Util.getShouldUseId3Tags(context) ? - search3(criteria, context, progressListener) : - search2(criteria, context, progressListener); - } catch (ApiNotSupportedException ignored) { - // 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 { - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi().search(null, null, null, criteria.getQuery(), - criteria.getSongCount(), null, null).execute(); - checkResponseSuccessful(response); - - return APISearchConverter.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 { - if (criteria.getQuery() == null) { - throw new IllegalArgumentException("Query param is null"); - } - - 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 APISearchConverter.toDomainEntity(response.body().getSearchResult()); - } - - private SearchResult search3(SearchCriteria criteria, - Context context, - ProgressListener progressListener) throws Exception { - if (criteria.getQuery() == null) { - throw new IllegalArgumentException("Query param is null"); - } - - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi().search3(criteria.getQuery(), - criteria.getArtistCount(), null, criteria.getAlbumCount(), null, - criteria.getSongCount(), null).execute(); - checkResponseSuccessful(response); - - return APISearchConverter.toDomainEntity(response.body().getSearchResult()); - } - - @Override - public MusicDirectory getPlaylist(String id, - String name, - Context context, - ProgressListener progressListener) throws Exception { - if (id == null) { - throw new IllegalArgumentException("id param is null!"); - } - - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .getPlaylist(id).execute(); - checkResponseSuccessful(response); - - MusicDirectory playlist = APIPlaylistConverter - .toMusicDirectoryDomainEntity(response.body().getPlaylist()); - savePlaylist(name, context, playlist); - return playlist; - } - - private void savePlaylist(String name, - Context context, - MusicDirectory playlist) throws IOException { - File playlistFile = FileUtil.getPlaylistFile(context, activeServerProvider.getValue().getActiveServer().getName(), 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) { - Timber.w("Failed to save playlist: %s", name); - throw e; - } finally { - bw.close(); - fw.close(); - } - } - - @Override - public List getPlaylists(boolean refresh, - Context context, - ProgressListener progressListener) throws Exception { - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .getPlaylists(null).execute(); - checkResponseSuccessful(response); - - return APIPlaylistConverter.toDomainEntitiesList(response.body().getPlaylists()); - } - - @Override - public void createPlaylist(String id, - String name, - List entries, - Context context, - ProgressListener progressListener) throws Exception { - List pSongIds = new ArrayList<>(entries.size()); - for (MusicDirectory.Entry entry : entries) { - if (entry.getId() != null) { - pSongIds.add(entry.getId()); - } - } - - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .createPlaylist(id, name, pSongIds).execute(); - checkResponseSuccessful(response); - } - - @Override - public void deletePlaylist(String id, - Context context, - ProgressListener progressListener) throws Exception { - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .deletePlaylist(id).execute(); - checkResponseSuccessful(response); - } - - @Override - public void updatePlaylist(String id, - String name, - String comment, - boolean pub, - Context context, - ProgressListener progressListener) throws Exception { - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .updatePlaylist(id, name, comment, pub, null, null).execute(); - checkResponseSuccessful(response); - } - - @Override - public List getPodcastsChannels(boolean refresh, - Context context, - ProgressListener progressListener) - throws Exception { - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .getPodcasts(false, null).execute(); - checkResponseSuccessful(response); - - return APIPodcastConverter.toDomainEntitiesList(response.body().getPodcastChannels()); - } - - @Override - public MusicDirectory getPodcastEpisodes(String podcastChannelId, - Context context, - ProgressListener progressListener) throws Exception { - if (podcastChannelId == null) { - throw new IllegalArgumentException("Podcast channel id is null!"); - } - - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .getPodcasts(true, podcastChannelId).execute(); - checkResponseSuccessful(response); - - List podcastEntries = response.body().getPodcastChannels().get(0) - .getEpisodeList(); - MusicDirectory musicDirectory = new MusicDirectory(); - for (MusicDirectoryChild podcastEntry : podcastEntries) { - if (!"skipped".equals(podcastEntry.getStatus()) && - !"error".equals(podcastEntry.getStatus())) { - MusicDirectory.Entry entry = APIMusicDirectoryConverter.toDomainEntity(podcastEntry); - entry.setTrack(null); - musicDirectory.addChild(entry); - } - } - return musicDirectory; - } - - @Override - public Lyrics getLyrics(String artist, - String title, - Context context, - ProgressListener progressListener) throws Exception { - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .getLyrics(artist, title).execute(); - checkResponseSuccessful(response); - - return APILyricsConverter.toDomainEntity(response.body().getLyrics()); - } - - @Override - public void scrobble(String id, - boolean submission, - Context context, - ProgressListener progressListener) throws Exception { - if (id == null) { - throw new IllegalArgumentException("Scrobble id is null"); - } - - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .scrobble(id, null, submission).execute(); - checkResponseSuccessful(response); - } - - @Override - public MusicDirectory getAlbumList(String type, - int size, - int offset, - Context context, - ProgressListener progressListener) throws Exception { - if (type == null) { - throw new IllegalArgumentException("Type is null!"); - } - - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .getAlbumList(AlbumListType.fromName(type), size, offset, null, - null, null, null).execute(); - checkResponseSuccessful(response); - - List childList = APIMusicDirectoryConverter - .toDomainEntityList(response.body().getAlbumList()); - MusicDirectory result = new MusicDirectory(); - result.addAll(childList); - return result; - } - - @Override - public MusicDirectory getAlbumList2(String type, - int size, - int offset, - Context context, - ProgressListener progressListener) throws Exception { - if (type == null) { - throw new IllegalArgumentException("Type is null!"); - } - - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .getAlbumList2(AlbumListType.fromName(type), size, offset, null, null, - null, null).execute(); - checkResponseSuccessful(response); - - MusicDirectory result = new MusicDirectory(); - result.addAll(APIAlbumConverter.toDomainEntityList(response.body().getAlbumList())); - return result; - } - - @Override - public MusicDirectory getRandomSongs(int size, - Context context, - ProgressListener progressListener) throws Exception { - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .getRandomSongs(size, null, null, null, null).execute(); - checkResponseSuccessful(response); - - MusicDirectory result = new MusicDirectory(); - result.addAll(APIMusicDirectoryConverter.toDomainEntityList(response.body().getSongsList())); - return result; - } - - @Override - public SearchResult getStarred(Context context, - ProgressListener progressListener) throws Exception { - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .getStarred(null).execute(); - checkResponseSuccessful(response); - - return APISearchConverter.toDomainEntity(response.body().getStarred()); - } - - @Override - public SearchResult getStarred2(Context context, - ProgressListener progressListener) throws Exception { - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .getStarred2(null).execute(); - checkResponseSuccessful(response); - - return APISearchConverter.toDomainEntity(response.body().getStarred2()); - } - - @Override - public Bitmap getCoverArt(Context context, - final MusicDirectory.Entry entry, - int size, - boolean saveToFile, - boolean highQuality, - ProgressListener progressListener) throws Exception { - // Synchronize on the entry so that we don't download concurrently for - // the same song. - if (entry == null) { - return null; - } - - synchronized (entry) { - // Use cached file, if existing. - Bitmap bitmap = FileUtil.getAlbumArtBitmap(context, entry, size, highQuality); - boolean serverScaling = ActiveServerProvider.Companion.isServerScalingEnabled(context); - - if (bitmap == null) { - Timber.d("Loading cover art for: %s", entry); - - final String id = entry.getCoverArt(); - if (TextUtils.isEmpty(id)) { - return null; // Can't load - } - - StreamResponse response = subsonicAPIClient.getCoverArt(id, (long) size); - checkStreamResponseError(response); - - if (response.getStream() == null) { - return null; // Failed to load - } - - InputStream in = null; - try { - in = response.getStream(); - byte[] bytes = Util.toByteArray(in); - - // If we aren't allowing server-side scaling, always save the file to disk because it will be unmodified - if (!serverScaling || saveToFile) { - OutputStream out = null; - - try { - out = new FileOutputStream(FileUtil.getAlbumArtFile(context, entry)); - out.write(bytes); - } finally { - Util.close(out); - } - } - - bitmap = FileUtil.getSampledBitmap(bytes, size, highQuality); - } finally { - Util.close(in); - } - } - - // Return scaled bitmap - return Util.scaleBitmap(bitmap, size); - } - } - - private void checkStreamResponseError(StreamResponse response) - throws SubsonicRESTException, IOException { - if (response.hasError() || response.getStream() == null) { - if (response.getApiError() != null) { - throw new SubsonicRESTException(response.getApiError()); - } else { - throw new IOException("Failed to make endpoint request, code: " + - response.getResponseHttpCode()); - } - } - } - - @Override - public Pair getDownloadInputStream(final Context context, - final MusicDirectory.Entry song, - final long offset, - final int maxBitrate, - final CancellableTask task) - throws Exception { - if (song == null) { - throw new IllegalArgumentException("Song for download is null!"); - } - long songOffset = offset < 0 ? 0 : offset; - - StreamResponse response = subsonicAPIClient.stream(song.getId(), maxBitrate, songOffset); - checkStreamResponseError(response); - if (response.getStream() == null) { - throw new IOException("Null stream response"); - } - Boolean partial = response.getResponseHttpCode() == 206; - - return new Pair<>(response.getStream(), partial); - } - - @Override - public String getVideoUrl(final Context context, - final String id, - final boolean useFlash) throws Exception { - // This method should not exists as video should be loaded using stream method - // Previous method implementation uses assumption that video will be available - // by videoPlayer.view?id=&maxBitRate=500&autoplay=true, but this url is not - // official Subsonic API call. - if (id == null) { - throw new IllegalArgumentException("Id is null"); - } - final String[] expectedResult = new String[1]; - expectedResult[0] = null; - final CountDownLatch latch = new CountDownLatch(1); - - new Thread(new Runnable() { - @Override - public void run() { - expectedResult[0] = subsonicAPIClient.getStreamUrl(id) + "&format=raw"; - latch.countDown(); - } - }, "Get-Video-Url").start(); - - latch.await(3, TimeUnit.SECONDS); - return expectedResult[0]; - } - - @Override - public JukeboxStatus updateJukeboxPlaylist(List ids, - Context context, - ProgressListener progressListener) throws Exception { - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .jukeboxControl(JukeboxAction.SET, null, null, ids, null) - .execute(); - checkResponseSuccessful(response); - - return APIJukeboxConverter.toDomainEntity(response.body().getJukebox()); - } - - @Override - public JukeboxStatus skipJukebox(int index, - int offsetSeconds, - Context context, - ProgressListener progressListener) throws Exception { - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .jukeboxControl(JukeboxAction.SKIP, index, offsetSeconds, null, null) - .execute(); - checkResponseSuccessful(response); - - return APIJukeboxConverter.toDomainEntity(response.body().getJukebox()); - } - - @Override - public JukeboxStatus stopJukebox(Context context, - ProgressListener progressListener) throws Exception { - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .jukeboxControl(JukeboxAction.STOP, null, null, null, null) - .execute(); - checkResponseSuccessful(response); - - return APIJukeboxConverter.toDomainEntity(response.body().getJukebox()); - } - - @Override - public JukeboxStatus startJukebox(Context context, - ProgressListener progressListener) throws Exception { - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .jukeboxControl(JukeboxAction.START, null, null, null, null) - .execute(); - checkResponseSuccessful(response); - - return APIJukeboxConverter.toDomainEntity(response.body().getJukebox()); - } - - @Override - public JukeboxStatus getJukeboxStatus(Context context, - ProgressListener progressListener) throws Exception { - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .jukeboxControl(JukeboxAction.STATUS, null, null, null, null) - .execute(); - checkResponseSuccessful(response); - - return APIJukeboxConverter.toDomainEntity(response.body().getJukebox()); - } - - @Override - public JukeboxStatus setJukeboxGain(float gain, Context context, - ProgressListener progressListener) throws Exception { - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .jukeboxControl(JukeboxAction.SET_GAIN, null, null, null, gain) - .execute(); - checkResponseSuccessful(response); - - return APIJukeboxConverter.toDomainEntity(response.body().getJukebox()); - } - - @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()); - } - - @Override - public List getGenres(boolean refresh, Context context, - ProgressListener progressListener) throws Exception { - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi().getGenres().execute(); - checkResponseSuccessful(response); - - return ApiGenreConverter.toDomainEntityList(response.body().getGenresList()); - } - - @Override - public MusicDirectory getSongsByGenre(String genre, - int count, - int offset, - Context context, - ProgressListener progressListener) throws Exception { - if (genre == null) { - throw new IllegalArgumentException("Genre is null"); - } - - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .getSongsByGenre(genre, count, offset, null) - .execute(); - checkResponseSuccessful(response); - - MusicDirectory result = new MusicDirectory(); - result.addAll(APIMusicDirectoryConverter.toDomainEntityList(response.body().getSongsList())); - return result; - } - - @Override - public UserInfo getUser(String username, - Context context, - ProgressListener progressListener) throws Exception { - if (username == null) { - throw new IllegalArgumentException("Username is null"); - } - - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .getUser(username).execute(); - checkResponseSuccessful(response); - - return APIUserConverter.toDomainEntity(response.body().getUser()); - } - - @Override - public List getChatMessages(Long since, - Context context, - ProgressListener progressListener) throws Exception { - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .getChatMessages(since).execute(); - checkResponseSuccessful(response); - - return APIChatMessageConverter.toDomainEntitiesList(response.body().getChatMessages()); - } - - @Override - public void addChatMessage(String message, - Context context, - ProgressListener progressListener) throws Exception { - if (message == null) { - throw new IllegalArgumentException("Message is null"); - } - - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .addChatMessage(message).execute(); - checkResponseSuccessful(response); - } - - @Override - public List getBookmarks(Context context, - ProgressListener progressListener) throws Exception { - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .getBookmarks().execute(); - checkResponseSuccessful(response); - - return APIBookmarkConverter.toDomainEntitiesList(response.body().getBookmarkList()); - } - - @Override - public void createBookmark(String id, - int position, - Context context, - ProgressListener progressListener) throws Exception { - if (id == null) { - throw new IllegalArgumentException("Item id should not be null"); - } - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .createBookmark(id, position, null).execute(); - checkResponseSuccessful(response); - } - - @Override - public void deleteBookmark(String id, - Context context, - ProgressListener progressListener) throws Exception { - if (id == null) { - throw new IllegalArgumentException("Id is null"); - } - Integer itemId = Integer.parseInt(id); - - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .deleteBookmark(id).execute(); - checkResponseSuccessful(response); - } - - @Override - public MusicDirectory getVideos(boolean refresh, - Context context, - ProgressListener progressListener) throws Exception { - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .getVideos().execute(); - checkResponseSuccessful(response); - - MusicDirectory musicDirectory = new MusicDirectory(); - musicDirectory.addAll(APIMusicDirectoryConverter - .toDomainEntityList(response.body().getVideosList())); - return musicDirectory; - } - - @Override - public List createShare(List ids, - String description, - Long expires, - Context context, - ProgressListener progressListener) throws Exception { - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .createShare(ids, description, expires).execute(); - checkResponseSuccessful(response); - - return APIShareConverter.toDomainEntitiesList(response.body().getShares()); - } - - @Override - public void deleteShare(String id, - Context context, - ProgressListener progressListener) throws Exception { - if (id == null) { - throw new IllegalArgumentException("Id is null!"); - } - Long shareId = Long.valueOf(id); - - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi().deleteShare(id).execute(); - checkResponseSuccessful(response); - } - - @Override - public void updateShare(String id, - String description, - Long expires, - Context context, - ProgressListener progressListener) throws Exception { - if (id == null) { - throw new IllegalArgumentException("Id is null"); - } - if (expires != null && - expires == 0) { - expires = null; - } - - updateProgressListener(progressListener, R.string.parser_reading); - Response response = subsonicAPIClient.getApi() - .updateShare(id, description, expires).execute(); - checkResponseSuccessful(response); - } - - @Override - public Bitmap getAvatar(final Context context, - final String username, - final int size, - final boolean saveToFile, - final boolean highQuality, - final ProgressListener progressListener) throws Exception { - // Synchronize on the username so that we don't download concurrently for - // the same user. - if (username == null) { - return null; - } - - synchronized (username) { - // Use cached file, if existing. - Bitmap bitmap = FileUtil.getAvatarBitmap(context, username, size, highQuality); - - if (bitmap == null) { - InputStream in = null; - try { - updateProgressListener(progressListener, R.string.parser_reading); - StreamResponse response = subsonicAPIClient.getAvatar(username); - if (response.hasError()) { - return null; - } - in = response.getStream(); - byte[] bytes = Util.toByteArray(in); - - // If we aren't allowing server-side scaling, always save the file to disk because it will be unmodified - if (saveToFile) { - OutputStream out = null; - - try { - out = new FileOutputStream(FileUtil.getAvatarFile(context, username)); - out.write(bytes); - } finally { - Util.close(out); - } - } - - bitmap = FileUtil.getSampledBitmap(bytes, size, highQuality); - } finally { - Util.close(in); - } - } - - // Return scaled bitmap - return Util.scaleBitmap(bitmap, size); - } - } - - private void updateProgressListener(@Nullable final ProgressListener progressListener, - @StringRes final int messageId) { - if (progressListener != null) { - progressListener.updateProgress(messageId); - } - } - - private void checkResponseSuccessful(@NonNull final Response response) - throws SubsonicRESTException, IOException { - if (response.isSuccessful() && - response.body().getStatus() == SubsonicResponse.Status.OK) { - return; - } - - if (!response.isSuccessful()) { - throw new IOException("Server error, code: " + response.code()); - } else if (response.body().getStatus() == SubsonicResponse.Status.ERROR && - response.body().getError() != null) { - throw new SubsonicRESTException(response.body().getError()); - } else { - throw new IOException("Failed to perform request: " + response.code()); - } - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/EditServerActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/EditServerActivity.kt index dd6b5ef8..82ec9b08 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/EditServerActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/EditServerActivity.kt @@ -9,7 +9,6 @@ import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Observer import com.google.android.material.switchmaterial.SwitchMaterial import com.google.android.material.textfield.TextInputLayout -import java.io.IOException import java.net.MalformedURLException import java.net.URL import org.koin.android.ext.android.inject @@ -19,16 +18,14 @@ import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration -import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ServerSetting +import org.moire.ultrasonic.service.ApiCallResponseChecker.Companion.checkResponseSuccessful import org.moire.ultrasonic.service.MusicServiceFactory -import org.moire.ultrasonic.service.SubsonicRESTException import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.ErrorDialog import org.moire.ultrasonic.util.ModalBackgroundTask import org.moire.ultrasonic.util.Util -import retrofit2.Response import timber.log.Timber /** @@ -87,6 +84,11 @@ internal class EditServerActivity : AppCompatActivity() { if (t != null) { currentServerSetting = t setFields() + // Remove the minimum API version so it can be detected again + if (currentServerSetting?.minimumApiVersion != null) { + currentServerSetting!!.minimumApiVersion = null + serverSettingsModel.updateItem(currentServerSetting) + } } } ) @@ -261,7 +263,8 @@ internal class EditServerActivity : AppCompatActivity() { ) val subsonicApiClient = SubsonicAPIClient(configuration) - // Execute a ping to retrieve the API version. This is accepted to fail if the authentication is incorrect yet. + // Execute a ping to retrieve the API version. + // This is accepted to fail if the authentication is incorrect yet. var pingResponse = subsonicApiClient.api.ping().execute() if (pingResponse?.body() != null) { val restApiVersion = pingResponse.body()!!.version.restApiVersion @@ -302,28 +305,6 @@ internal class EditServerActivity : AppCompatActivity() { task.execute() } - /** - * Checks the Subsonic Response for application specific errors - */ - private fun checkResponseSuccessful(response: Response) { - if ( - response.isSuccessful && - response.body()!!.status === SubsonicResponse.Status.OK - ) { - return - } - if (!response.isSuccessful) { - throw IOException("Server error, code: " + response.code()) - } else if ( - response.body()!!.status === SubsonicResponse.Status.ERROR && - response.body()!!.error != null - ) { - throw SubsonicRESTException(response.body()!!.error!!) - } else { - throw IOException("Failed to perform request: " + response.code()) - } - } - /** * Finishes the Activity, after confirmation from the user if needed */ diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt index be2431d3..fba4b146 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt @@ -13,6 +13,7 @@ import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration import org.moire.ultrasonic.cache.PermanentFileStorage import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.log.TimberOkHttpLogger +import org.moire.ultrasonic.service.ApiCallResponseChecker import org.moire.ultrasonic.service.CachedMusicService import org.moire.ultrasonic.service.MusicService import org.moire.ultrasonic.service.OfflineMusicService @@ -59,13 +60,14 @@ val musicServiceModule = module { single { TimberOkHttpLogger() } single { SubsonicAPIClient(get(), get()) } + single { ApiCallResponseChecker(get(), get()) } single(named(ONLINE_MUSIC_SERVICE)) { - CachedMusicService(RESTMusicService(get(), get())) + CachedMusicService(RESTMusicService(get(), get(), get(), get())) } single(named(OFFLINE_MUSIC_SERVICE)) { - OfflineMusicService(get(), get()) + OfflineMusicService() } single { SubsonicImageLoader(androidContext(), get()) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/ApiCallResponseChecker.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/ApiCallResponseChecker.kt new file mode 100644 index 00000000..74baeec5 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/ApiCallResponseChecker.kt @@ -0,0 +1,66 @@ +package org.moire.ultrasonic.service + +import java.io.IOException +import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient +import org.moire.ultrasonic.api.subsonic.SubsonicAPIDefinition +import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse +import org.moire.ultrasonic.data.ActiveServerProvider +import retrofit2.Response +import timber.log.Timber + +/** + * This call wraps Subsonic API calls so their results can be checked for errors, API version, etc + */ +class ApiCallResponseChecker( + private val subsonicAPIClient: SubsonicAPIClient, + private val activeServerProvider: ActiveServerProvider +) { + /** + * Executes a Subsonic API call with response check + */ + @Throws(SubsonicRESTException::class, IOException::class) + fun > callWithResponseCheck( + call: (SubsonicAPIDefinition) -> T + ): T { + // Check for API version when first contacting the server + if (activeServerProvider.getActiveServer().minimumApiVersion == null) { + try { + val response = subsonicAPIClient.api.ping().execute() + if (response?.body() != null) { + val restApiVersion = response.body()!!.version.restApiVersion + Timber.i("Server minimum API version set to %s", restApiVersion) + activeServerProvider.setMinimumApiVersion(restApiVersion) + } + } catch (ignored: Exception) { + // This Ping is only used to get the API Version, if it fails, that's no problem. + } + } + + // This call will be now executed with the correct API Version, so it shouldn't fail + val result = call.invoke(subsonicAPIClient.api) + checkResponseSuccessful(result) + return result + } + + /** + * Creates Exceptions from the results returned by the Subsonic API + */ + companion object { + @Throws(SubsonicRESTException::class, IOException::class) + fun checkResponseSuccessful(response: Response) { + if (response.isSuccessful && response.body()!!.status === SubsonicResponse.Status.OK) { + return + } + if (!response.isSuccessful) { + throw IOException("Server error, code: " + response.code()) + } else if ( + response.body()!!.status === SubsonicResponse.Status.ERROR && + response.body()!!.error != null + ) { + throw SubsonicRESTException(response.body()!!.error!!) + } else { + throw IOException("Failed to perform request: " + response.code()) + } + } + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt new file mode 100644 index 00000000..f100d88e --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt @@ -0,0 +1,1094 @@ +/* + 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 + +import android.content.Context +import android.graphics.Bitmap +import android.text.TextUtils +import androidx.annotation.StringRes +import java.io.BufferedWriter +import java.io.File +import java.io.FileOutputStream +import java.io.FileWriter +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import org.moire.ultrasonic.R +import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException +import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient +import org.moire.ultrasonic.api.subsonic.models.AlbumListType.Companion.fromName +import org.moire.ultrasonic.api.subsonic.models.JukeboxAction +import org.moire.ultrasonic.api.subsonic.response.StreamResponse +import org.moire.ultrasonic.cache.PermanentFileStorage +import org.moire.ultrasonic.cache.serializers.getIndexesSerializer +import org.moire.ultrasonic.cache.serializers.getMusicFolderListSerializer +import org.moire.ultrasonic.data.ActiveServerProvider +import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline +import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isServerScalingEnabled +import org.moire.ultrasonic.domain.Bookmark +import org.moire.ultrasonic.domain.ChatMessage +import org.moire.ultrasonic.domain.Genre +import org.moire.ultrasonic.domain.Indexes +import org.moire.ultrasonic.domain.JukeboxStatus +import org.moire.ultrasonic.domain.Lyrics +import org.moire.ultrasonic.domain.MusicDirectory +import org.moire.ultrasonic.domain.MusicFolder +import org.moire.ultrasonic.domain.Playlist +import org.moire.ultrasonic.domain.PodcastsChannel +import org.moire.ultrasonic.domain.SearchCriteria +import org.moire.ultrasonic.domain.SearchResult +import org.moire.ultrasonic.domain.Share +import org.moire.ultrasonic.domain.UserInfo +import org.moire.ultrasonic.domain.toDomainEntitiesList +import org.moire.ultrasonic.domain.toDomainEntity +import org.moire.ultrasonic.domain.toDomainEntityList +import org.moire.ultrasonic.domain.toMusicDirectoryDomainEntity +import org.moire.ultrasonic.util.CancellableTask +import org.moire.ultrasonic.util.FileUtil +import org.moire.ultrasonic.util.ProgressListener +import org.moire.ultrasonic.util.Util +import timber.log.Timber + +/** + * @author Sindre Mehus + */ +open class RESTMusicService( + private val subsonicAPIClient: SubsonicAPIClient, + private val fileStorage: PermanentFileStorage, + private val activeServerProvider: ActiveServerProvider, + private val responseChecker: ApiCallResponseChecker +) : MusicService { + + @Throws(Exception::class) + override fun ping(context: Context, progressListener: ProgressListener?) { + updateProgressListener(progressListener, R.string.service_connecting) + + responseChecker.callWithResponseCheck { api -> api.ping().execute() } + } + + @Throws(Exception::class) + override fun isLicenseValid(context: Context, progressListener: ProgressListener?): Boolean { + updateProgressListener(progressListener, R.string.service_connecting) + + val response = responseChecker.callWithResponseCheck { api -> api.getLicense().execute() } + + return response.body()!!.license.valid + } + + @Throws(Exception::class) + override fun getMusicFolders( + refresh: Boolean, + context: Context, + progressListener: ProgressListener? + ): List { + val cachedMusicFolders = fileStorage.load( + MUSIC_FOLDER_STORAGE_NAME, getMusicFolderListSerializer() + ) + + if (cachedMusicFolders != null && !refresh) return cachedMusicFolders + + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> + api.getMusicFolders().execute() + } + + val musicFolders = response.body()!!.musicFolders.toDomainEntityList() + fileStorage.store(MUSIC_FOLDER_STORAGE_NAME, musicFolders, getMusicFolderListSerializer()) + + return musicFolders + } + + @Throws(Exception::class) + override fun getIndexes( + musicFolderId: String?, + refresh: Boolean, + context: Context, + progressListener: ProgressListener? + ): Indexes { + val cachedIndexes = fileStorage.load(INDEXES_STORAGE_NAME, getIndexesSerializer()) + if (cachedIndexes != null && !refresh) return cachedIndexes + + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> + api.getIndexes(musicFolderId, null).execute() + } + + val indexes = response.body()!!.indexes.toDomainEntity() + fileStorage.store(INDEXES_STORAGE_NAME, indexes, getIndexesSerializer()) + return indexes + } + + @Throws(Exception::class) + override fun getArtists( + refresh: Boolean, + context: Context, + progressListener: ProgressListener? + ): Indexes { + val cachedArtists = fileStorage.load(ARTISTS_STORAGE_NAME, getIndexesSerializer()) + if (cachedArtists != null && !refresh) return cachedArtists + + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> + api.getArtists(null).execute() + } + + val indexes = response.body()!!.indexes.toDomainEntity() + fileStorage.store(ARTISTS_STORAGE_NAME, indexes, getIndexesSerializer()) + return indexes + } + + @Throws(Exception::class) + override fun star( + id: String?, + albumId: String?, + artistId: String?, + context: Context, + progressListener: ProgressListener? + ) { + updateProgressListener(progressListener, R.string.parser_reading) + + responseChecker.callWithResponseCheck { api -> api.star(id, albumId, artistId).execute() } + } + + @Throws(Exception::class) + override fun unstar( + id: String?, + albumId: String?, + artistId: String?, + context: Context, + progressListener: ProgressListener? + ) { + updateProgressListener(progressListener, R.string.parser_reading) + + responseChecker.callWithResponseCheck { api -> api.unstar(id, albumId, artistId).execute() } + } + + @Throws(Exception::class) + override fun setRating( + id: String, + rating: Int, + context: Context, + progressListener: ProgressListener? + ) { + updateProgressListener(progressListener, R.string.parser_reading) + + responseChecker.callWithResponseCheck { api -> api.setRating(id, rating).execute() } + } + + @Throws(Exception::class) + override fun getMusicDirectory( + id: String, + name: String?, + refresh: Boolean, + context: Context, + progressListener: ProgressListener? + ): MusicDirectory { + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> + api.getMusicDirectory(id).execute() + } + + return response.body()!!.musicDirectory.toDomainEntity() + } + + @Throws(Exception::class) + override fun getArtist( + id: String, + name: String?, + refresh: Boolean, + context: Context, + progressListener: ProgressListener? + ): MusicDirectory { + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> api.getArtist(id).execute() } + + return response.body()!!.artist.toMusicDirectoryDomainEntity() + } + + @Throws(Exception::class) + override fun getAlbum( + id: String, + name: String?, + refresh: Boolean, + context: Context, + progressListener: ProgressListener? + ): MusicDirectory { + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> api.getAlbum(id).execute() } + + return response.body()!!.album.toMusicDirectoryDomainEntity() + } + + @Throws(Exception::class) + override fun search( + criteria: SearchCriteria, + context: Context, + progressListener: ProgressListener? + ): SearchResult { + return try { + if ( + !isOffline(context) && + Util.getShouldUseId3Tags(context) + ) search3(criteria, progressListener) + else search2(criteria, progressListener) + } catch (ignored: ApiNotSupportedException) { + // Ensure backward compatibility with REST 1.3. + searchOld(criteria, progressListener) + } + } + + /** + * Search using the "search" REST method. + */ + @Throws(Exception::class) + private fun searchOld( + criteria: SearchCriteria, + progressListener: ProgressListener? + ): SearchResult { + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> + api.search(null, null, null, criteria.query, criteria.songCount, null, null) + .execute() + } + + return response.body()!!.searchResult.toDomainEntity() + } + + /** + * Search using the "search2" REST method, available in 1.4.0 and later. + */ + @Throws(Exception::class) + private fun search2( + criteria: SearchCriteria, + progressListener: ProgressListener? + ): SearchResult { + requireNotNull(criteria.query) { "Query param is null" } + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> + api.search2( + criteria.query, criteria.artistCount, null, criteria.albumCount, null, + criteria.songCount, null + ).execute() + } + + return response.body()!!.searchResult.toDomainEntity() + } + + @Throws(Exception::class) + private fun search3( + criteria: SearchCriteria, + progressListener: ProgressListener? + ): SearchResult { + requireNotNull(criteria.query) { "Query param is null" } + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> + api.search3( + criteria.query, criteria.artistCount, null, criteria.albumCount, null, + criteria.songCount, null + ).execute() + } + + return response.body()!!.searchResult.toDomainEntity() + } + + @Throws(Exception::class) + override fun getPlaylist( + id: String, + name: String?, + context: Context, + progressListener: ProgressListener? + ): MusicDirectory { + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> + api.getPlaylist(id).execute() + } + + val playlist = response.body()!!.playlist.toMusicDirectoryDomainEntity() + savePlaylist(name, context, playlist) + + return playlist + } + + @Throws(IOException::class) + private fun savePlaylist( + name: String?, + context: Context, + playlist: MusicDirectory + ) { + val playlistFile = FileUtil.getPlaylistFile( + context, activeServerProvider.getActiveServer().name, name + ) + + val fw = FileWriter(playlistFile) + val bw = BufferedWriter(fw) + + try { + fw.write("#EXTM3U\n") + for (e in playlist.getChildren()) { + var filePath = FileUtil.getSongFile(context, e).absolutePath + + if (!File(filePath).exists()) { + val ext = FileUtil.getExtension(filePath) + val base = FileUtil.getBaseName(filePath) + filePath = "$base.complete.$ext" + } + fw.write(filePath + "\n") + } + } catch (e: IOException) { + Timber.w("Failed to save playlist: %s", name) + throw e + } finally { + bw.close() + fw.close() + } + } + + @Throws(Exception::class) + override fun getPlaylists( + refresh: Boolean, + context: Context, + progressListener: ProgressListener? + ): List { + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> + api.getPlaylists(null).execute() + } + + return response.body()!!.playlists.toDomainEntitiesList() + } + + @Throws(Exception::class) + override fun createPlaylist( + id: String?, + name: String?, + entries: List, + context: Context, + progressListener: ProgressListener? + ) { + val pSongIds: MutableList = ArrayList(entries.size) + + for ((id1) in entries) { + if (id1 != null) { + pSongIds.add(id1) + } + } + + updateProgressListener(progressListener, R.string.parser_reading) + + responseChecker.callWithResponseCheck { api -> + api.createPlaylist(id, name, pSongIds.toList()).execute() + } + } + + @Throws(Exception::class) + override fun deletePlaylist( + id: String, + context: Context, + progressListener: ProgressListener? + ) { + updateProgressListener(progressListener, R.string.parser_reading) + + responseChecker.callWithResponseCheck { api -> api.deletePlaylist(id).execute() } + } + + @Throws(Exception::class) + override fun updatePlaylist( + id: String, + name: String?, + comment: String?, + pub: Boolean, + context: Context?, + progressListener: ProgressListener? + ) { + updateProgressListener(progressListener, R.string.parser_reading) + + responseChecker.callWithResponseCheck { api -> + api.updatePlaylist(id, name, comment, pub, null, null) + .execute() + } + } + + @Throws(Exception::class) + override fun getPodcastsChannels( + refresh: Boolean, + context: Context, + progressListener: ProgressListener? + ): List { + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> + api.getPodcasts(false, null).execute() + } + + return response.body()!!.podcastChannels.toDomainEntitiesList() + } + + @Throws(Exception::class) + override fun getPodcastEpisodes( + podcastChannelId: String?, + context: Context, + progressListener: ProgressListener? + ): MusicDirectory { + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> + api.getPodcasts(true, podcastChannelId).execute() + } + + val podcastEntries = response.body()!!.podcastChannels[0].episodeList + val musicDirectory = MusicDirectory() + + for (podcastEntry in podcastEntries) { + if ( + "skipped" != podcastEntry.status && + "error" != podcastEntry.status + ) { + val entry = podcastEntry.toDomainEntity() + entry.track = null + musicDirectory.addChild(entry) + } + } + + return musicDirectory + } + + @Throws(Exception::class) + override fun getLyrics( + artist: String?, + title: String?, + context: Context, + progressListener: ProgressListener? + ): Lyrics { + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> + api.getLyrics(artist, title).execute() + } + + return response.body()!!.lyrics.toDomainEntity() + } + + @Throws(Exception::class) + override fun scrobble( + id: String, + submission: Boolean, + context: Context, + progressListener: ProgressListener? + ) { + updateProgressListener(progressListener, R.string.parser_reading) + + responseChecker.callWithResponseCheck { api -> + api.scrobble(id, null, submission).execute() + } + } + + @Throws(Exception::class) + override fun getAlbumList( + type: String, + size: Int, + offset: Int, + context: Context, + progressListener: ProgressListener? + ): MusicDirectory { + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> + api.getAlbumList(fromName(type), size, offset, null, null, null, null) + .execute() + } + + val childList = response.body()!!.albumList.toDomainEntityList() + val result = MusicDirectory() + result.addAll(childList) + + return result + } + + @Throws(Exception::class) + override fun getAlbumList2( + type: String, + size: Int, + offset: Int, + context: Context, + progressListener: ProgressListener? + ): MusicDirectory { + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> + api.getAlbumList2( + fromName(type), + size, + offset, + null, + null, + null, + null + ).execute() + } + + val result = MusicDirectory() + result.addAll(response.body()!!.albumList.toDomainEntityList()) + + return result + } + + @Throws(Exception::class) + override fun getRandomSongs( + size: Int, + context: Context, + progressListener: ProgressListener? + ): MusicDirectory { + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> + api.getRandomSongs( + size, + null, + null, + null, + null + ).execute() + } + + val result = MusicDirectory() + result.addAll(response.body()!!.songsList.toDomainEntityList()) + + return result + } + + @Throws(Exception::class) + override fun getStarred( + context: Context, + progressListener: ProgressListener? + ): SearchResult { + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> + api.getStarred(null).execute() + } + + return response.body()!!.starred.toDomainEntity() + } + + @Throws(Exception::class) + override fun getStarred2( + context: Context, + progressListener: ProgressListener? + ): SearchResult { + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> + api.getStarred2(null).execute() + } + + return response.body()!!.starred2.toDomainEntity() + } + + @Throws(Exception::class) + override fun getCoverArt( + context: Context, + entry: MusicDirectory.Entry?, + size: Int, + saveToFile: Boolean, + highQuality: Boolean, + progressListener: ProgressListener? + ): Bitmap? { + // Synchronize on the entry so that we don't download concurrently for + // the same song. + if (entry == null) { + return null + } + + synchronized(entry) { + // Use cached file, if existing. + var bitmap = FileUtil.getAlbumArtBitmap(context, entry, size, highQuality) + val serverScaling = isServerScalingEnabled(context) + + if (bitmap == null) { + Timber.d("Loading cover art for: %s", entry) + + val id = entry.coverArt + + if (TextUtils.isEmpty(id)) { + return null // Can't load + } + + val response = subsonicAPIClient.getCoverArt(id!!, size.toLong()) + checkStreamResponseError(response) + + if (response.stream == null) { + return null // Failed to load + } + + var inputStream: InputStream? = null + try { + inputStream = response.stream + val bytes = Util.toByteArray(inputStream) + + // If we aren't allowing server-side scaling, always save the file to disk + // because it will be unmodified + if (!serverScaling || saveToFile) { + var outputStream: OutputStream? = null + try { + outputStream = FileOutputStream( + FileUtil.getAlbumArtFile(context, entry) + ) + outputStream.write(bytes) + } finally { + Util.close(outputStream) + } + } + + bitmap = FileUtil.getSampledBitmap(bytes, size, highQuality) + } finally { + Util.close(inputStream) + } + } + + // Return scaled bitmap + return Util.scaleBitmap(bitmap, size) + } + } + + @Throws(SubsonicRESTException::class, IOException::class) + private fun checkStreamResponseError(response: StreamResponse) { + if (response.hasError() || response.stream == null) { + if (response.apiError != null) { + throw SubsonicRESTException(response.apiError!!) + } else { + throw IOException( + "Failed to make endpoint request, code: " + response.responseHttpCode + ) + } + } + } + + @Throws(Exception::class) + override fun getDownloadInputStream( + context: Context, + song: MusicDirectory.Entry, + offset: Long, + maxBitrate: Int, + task: CancellableTask + ): Pair { + val songOffset = if (offset < 0) 0 else offset + + val response = subsonicAPIClient.stream(song.id!!, maxBitrate, songOffset) + checkStreamResponseError(response) + + if (response.stream == null) { + throw IOException("Null stream response") + } + + val partial = response.responseHttpCode == 206 + return Pair(response.stream!!, partial) + } + + @Throws(Exception::class) + override fun getVideoUrl( + context: Context, + id: String, + useFlash: Boolean + ): String { + // This method should not exists as video should be loaded using stream method + // Previous method implementation uses assumption that video will be available + // by videoPlayer.view?id=&maxBitRate=500&autoplay=true, but this url is not + // official Subsonic API call. + val expectedResult = arrayOfNulls(1) + expectedResult[0] = null + + val latch = CountDownLatch(1) + + Thread( + { + expectedResult[0] = subsonicAPIClient.getStreamUrl(id) + "&format=raw" + latch.countDown() + }, + "Get-Video-Url" + ).start() + + latch.await(3, TimeUnit.SECONDS) + + return expectedResult[0]!! + } + + @Throws(Exception::class) + override fun updateJukeboxPlaylist( + ids: List?, + context: Context, + progressListener: ProgressListener? + ): JukeboxStatus { + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> + api.jukeboxControl(JukeboxAction.SET, null, null, ids, null) + .execute() + } + + return response.body()!!.jukebox.toDomainEntity() + } + + @Throws(Exception::class) + override fun skipJukebox( + index: Int, + offsetSeconds: Int, + context: Context, + progressListener: ProgressListener? + ): JukeboxStatus { + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> + api.jukeboxControl(JukeboxAction.SKIP, index, offsetSeconds, null, null) + .execute() + } + + return response.body()!!.jukebox.toDomainEntity() + } + + @Throws(Exception::class) + override fun stopJukebox( + context: Context, + progressListener: ProgressListener? + ): JukeboxStatus { + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> + api.jukeboxControl(JukeboxAction.STOP, null, null, null, null) + .execute() + } + + return response.body()!!.jukebox.toDomainEntity() + } + + @Throws(Exception::class) + override fun startJukebox( + context: Context, + progressListener: ProgressListener? + ): JukeboxStatus { + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> + api.jukeboxControl(JukeboxAction.START, null, null, null, null) + .execute() + } + + return response.body()!!.jukebox.toDomainEntity() + } + + @Throws(Exception::class) + override fun getJukeboxStatus( + context: Context, + progressListener: ProgressListener? + ): JukeboxStatus { + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> + api.jukeboxControl(JukeboxAction.STATUS, null, null, null, null) + .execute() + } + + return response.body()!!.jukebox.toDomainEntity() + } + + @Throws(Exception::class) + override fun setJukeboxGain( + gain: Float, + context: Context, + progressListener: ProgressListener? + ): JukeboxStatus { + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> + api.jukeboxControl(JukeboxAction.SET_GAIN, null, null, null, gain) + .execute() + } + + return response.body()!!.jukebox.toDomainEntity() + } + + @Throws(Exception::class) + override fun getShares( + refresh: Boolean, + context: Context, + progressListener: ProgressListener? + ): List { + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> api.getShares().execute() } + + return response.body()!!.shares.toDomainEntitiesList() + } + + @Throws(Exception::class) + override fun getGenres( + refresh: Boolean, + context: Context, + progressListener: ProgressListener? + ): List { + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> api.getGenres().execute() } + + return response.body()!!.genresList.toDomainEntityList() + } + + @Throws(Exception::class) + override fun getSongsByGenre( + genre: String, + count: Int, + offset: Int, + context: Context, + progressListener: ProgressListener? + ): MusicDirectory { + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> + api.getSongsByGenre(genre, count, offset, null).execute() + } + + val result = MusicDirectory() + result.addAll(response.body()!!.songsList.toDomainEntityList()) + + return result + } + + @Throws(Exception::class) + override fun getUser( + username: String, + context: Context, + progressListener: ProgressListener? + ): UserInfo { + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> + api.getUser(username).execute() + } + + return response.body()!!.user.toDomainEntity() + } + + @Throws(Exception::class) + override fun getChatMessages( + since: Long?, + context: Context, + progressListener: ProgressListener? + ): List { + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> + api.getChatMessages(since).execute() + } + + return response.body()!!.chatMessages.toDomainEntitiesList() + } + + @Throws(Exception::class) + override fun addChatMessage( + message: String, + context: Context, + progressListener: ProgressListener? + ) { + updateProgressListener(progressListener, R.string.parser_reading) + + responseChecker.callWithResponseCheck { api -> api.addChatMessage(message).execute() } + } + + @Throws(Exception::class) + override fun getBookmarks( + context: Context, + progressListener: ProgressListener? + ): List { + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> api.getBookmarks().execute() } + + return response.body()!!.bookmarkList.toDomainEntitiesList() + } + + @Throws(Exception::class) + override fun createBookmark( + id: String, + position: Int, + context: Context, + progressListener: ProgressListener? + ) { + updateProgressListener(progressListener, R.string.parser_reading) + + responseChecker.callWithResponseCheck { api -> + api.createBookmark(id, position.toLong(), null).execute() + } + } + + @Throws(Exception::class) + override fun deleteBookmark( + id: String, + context: Context, + progressListener: ProgressListener? + ) { + updateProgressListener(progressListener, R.string.parser_reading) + + responseChecker.callWithResponseCheck { api -> api.deleteBookmark(id).execute() } + } + + @Throws(Exception::class) + override fun getVideos( + refresh: Boolean, + context: Context, + progressListener: ProgressListener? + ): MusicDirectory { + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> api.getVideos().execute() } + + val musicDirectory = MusicDirectory() + musicDirectory.addAll(response.body()!!.videosList.toDomainEntityList()) + + return musicDirectory + } + + @Throws(Exception::class) + override fun createShare( + ids: List, + description: String?, + expires: Long?, + context: Context, + progressListener: ProgressListener? + ): List { + updateProgressListener(progressListener, R.string.parser_reading) + + val response = responseChecker.callWithResponseCheck { api -> + api.createShare(ids, description, expires).execute() + } + + return response.body()!!.shares.toDomainEntitiesList() + } + + @Throws(Exception::class) + override fun deleteShare( + id: String, + context: Context, + progressListener: ProgressListener? + ) { + updateProgressListener(progressListener, R.string.parser_reading) + + responseChecker.callWithResponseCheck { api -> api.deleteShare(id).execute() } + } + + @Throws(Exception::class) + override fun updateShare( + id: String, + description: String?, + expires: Long?, + context: Context, + progressListener: ProgressListener? + ) { + var expiresValue: Long? = expires + if (expires != null && expires == 0L) { + expiresValue = null + } + + updateProgressListener(progressListener, R.string.parser_reading) + + responseChecker.callWithResponseCheck { api -> + api.updateShare(id, description, expiresValue).execute() + } + } + + @Throws(Exception::class) + override fun getAvatar( + context: Context, + username: String?, + size: Int, + saveToFile: Boolean, + highQuality: Boolean, + progressListener: ProgressListener? + ): Bitmap? { + // Synchronize on the username so that we don't download concurrently for + // the same user. + if (username == null) { + return null + } + + synchronized(username) { + // Use cached file, if existing. + var bitmap = FileUtil.getAvatarBitmap(context, username, size, highQuality) + + if (bitmap == null) { + var inputStream: InputStream? = null + try { + updateProgressListener(progressListener, R.string.parser_reading) + val response = subsonicAPIClient.getAvatar(username) + + if (response.hasError()) return null + + inputStream = response.stream + val bytes = Util.toByteArray(inputStream) + + // If we aren't allowing server-side scaling, always save the file to disk + // because it will be unmodified + if (saveToFile) { + var outputStream: OutputStream? = null + + try { + outputStream = FileOutputStream( + FileUtil.getAvatarFile(context, username) + ) + outputStream.write(bytes) + } finally { + Util.close(outputStream) + } + } + + bitmap = FileUtil.getSampledBitmap(bytes, size, highQuality) + } finally { + Util.close(inputStream) + } + } + + // Return scaled bitmap + return Util.scaleBitmap(bitmap, size) + } + } + + private fun updateProgressListener( + progressListener: ProgressListener?, + @StringRes messageId: Int + ) { + progressListener?.updateProgress(messageId) + } + + companion object { + private const val MUSIC_FOLDER_STORAGE_NAME = "music_folder" + private const val INDEXES_STORAGE_NAME = "indexes" + private const val ARTISTS_STORAGE_NAME = "artists" + } +}