/* 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 android.util.Log; 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.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.Pair; import retrofit2.Response; /** * @author Sindre Mehus */ public class RESTMusicService implements MusicService { private static final String TAG = RESTMusicService.class.getSimpleName(); 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); 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 !Util.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(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 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 = Util.isServerScalingEnabled(context); if (bitmap == null) { Log.d(TAG, "Loading cover art for: " + 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(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(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(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()); } } }