/* 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 <http://www.gnu.org/licenses/>. Copyright 2009 (C) Sindre Mehus */ package github.daneren2005.dsub.service; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.Reader; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.util.Base64; import android.util.Log; import com.google.android.gms.security.ProviderInstaller; import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.*; import github.daneren2005.dsub.fragments.MainFragment; import github.daneren2005.dsub.service.parser.EntryListParser; import github.daneren2005.dsub.service.parser.ArtistInfoParser; import github.daneren2005.dsub.service.parser.BookmarkParser; import github.daneren2005.dsub.service.parser.ChatMessageParser; import github.daneren2005.dsub.service.parser.ErrorParser; import github.daneren2005.dsub.service.parser.GenreParser; import github.daneren2005.dsub.service.parser.IndexesParser; import github.daneren2005.dsub.service.parser.InternetRadioStationParser; import github.daneren2005.dsub.service.parser.JukeboxStatusParser; import github.daneren2005.dsub.service.parser.LicenseParser; import github.daneren2005.dsub.service.parser.LyricsParser; import github.daneren2005.dsub.service.parser.MusicDirectoryParser; import github.daneren2005.dsub.service.parser.MusicFoldersParser; import github.daneren2005.dsub.service.parser.PlayQueueParser; import github.daneren2005.dsub.service.parser.PlaylistParser; import github.daneren2005.dsub.service.parser.PlaylistsParser; import github.daneren2005.dsub.service.parser.PodcastChannelParser; import github.daneren2005.dsub.service.parser.PodcastEntryParser; import github.daneren2005.dsub.service.parser.RandomSongsParser; import github.daneren2005.dsub.service.parser.ScanStatusParser; import github.daneren2005.dsub.service.parser.SearchResult2Parser; import github.daneren2005.dsub.service.parser.SearchResultParser; import github.daneren2005.dsub.service.parser.ShareParser; import github.daneren2005.dsub.service.parser.StarredListParser; import github.daneren2005.dsub.service.parser.TopSongsParser; import github.daneren2005.dsub.service.parser.UserParser; import github.daneren2005.dsub.service.parser.VideosParser; import github.daneren2005.dsub.util.Pair; import github.daneren2005.dsub.util.SilentBackgroundTask; import github.daneren2005.dsub.util.Constants; import github.daneren2005.dsub.util.FileUtil; import github.daneren2005.dsub.util.ProgressListener; import github.daneren2005.dsub.util.SongDBHandler; import github.daneren2005.dsub.util.Util; import java.io.*; import java.util.Map; import java.util.zip.GZIPInputStream; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; public class RESTMusicService implements MusicService { private static final String TAG = RESTMusicService.class.getSimpleName(); private static final int SOCKET_READ_TIMEOUT_DEFAULT = 10 * 1000; private static final int SOCKET_READ_TIMEOUT_DOWNLOAD = 30 * 1000; private static final int SOCKET_READ_TIMEOUT_GET_PLAYLIST = 60 * 1000; // Allow 20 seconds extra timeout per MB offset. private static final double TIMEOUT_MILLIS_PER_OFFSET_BYTE = 20000.0 / 1000000.0; private static final int HTTP_REQUEST_MAX_ATTEMPTS = 5; private static final long REDIRECTION_CHECK_INTERVAL_MILLIS = 60L * 60L * 1000L; private SSLSocketFactory sslSocketFactory; private HostnameVerifier selfSignedHostnameVerifier; private long redirectionLastChecked; private int redirectionNetworkType = -1; private String redirectFrom; private String redirectTo; private Integer instance; private boolean hasInstalledGoogleSSL = false; public RESTMusicService() { TrustManager[] trustAllCerts = new TrustManager[]{ new X509TrustManager() { public java.security.cert.X509Certificate[] getAcceptedIssuers() { return null; } public void checkClientTrusted( java.security.cert.X509Certificate[] certs, String authType) { } public void checkServerTrusted( java.security.cert.X509Certificate[] certs, String authType) { } } }; try { SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); sslSocketFactory = sslContext.getSocketFactory(); } catch (Exception e) { } selfSignedHostnameVerifier = new HostnameVerifier() { public boolean verify(String hostname, SSLSession session) { return true; } }; } @Override public void ping(Context context, ProgressListener progressListener) throws Exception { Reader reader = getReader(context, progressListener, "ping"); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { Util.close(reader); } } @Override public boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception { Reader reader = getReader(context, progressListener, "getLicense"); try { ServerInfo serverInfo = new LicenseParser(context, getInstance(context)).parse(reader); return serverInfo.isLicenseValid(); } finally { Util.close(reader); } } public List<MusicFolder> getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception { Reader reader = getReader(context, progressListener, "getMusicFolders"); try { return new MusicFoldersParser(context, getInstance(context)).parse(reader, progressListener); } finally { Util.close(reader); } } @Override public void startRescan(Context context, ProgressListener listener) throws Exception { String startMethod = ServerInfo.isMadsonic(context, getInstance(context)) ? "startRescan" : "startScan"; String refreshMethod = null; if(ServerInfo.isMadsonic(context, getInstance(context))) { startMethod = "startRescan"; refreshMethod = "scanstatus"; } else { startMethod = "startScan"; refreshMethod = "getScanStatus"; } Reader reader = getReader(context, listener, startMethod); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { Util.close(reader); } // Now check if still running boolean done = false; while(!done) { reader = getReader(context, null, refreshMethod); try { boolean running = new ScanStatusParser(context, getInstance(context)).parse(reader, listener); if(running) { // Don't run system ragged trying to query too much Thread.sleep(100L); } else { done = true; } } catch(Exception e) { done = true; } finally { Util.close(reader); } } } @Override public Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception { List<String> parameterNames = new ArrayList<String>(); List<Object> parameterValues = new ArrayList<Object>(); if (musicFolderId != null) { parameterNames.add("musicFolderId"); parameterValues.add(musicFolderId); } Reader reader = getReader(context, progressListener, Util.isTagBrowsing(context, getInstance(context)) ? "getArtists" : "getIndexes", parameterNames, parameterValues); try { return new IndexesParser(context, getInstance(context)).parse(reader, progressListener); } finally { Util.close(reader); } } @Override public MusicDirectory getMusicDirectory(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception { SharedPreferences prefs = Util.getPreferences(context); String cacheLocn = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null); if(cacheLocn != null && id.indexOf(cacheLocn) != -1) { String search = Util.parseOfflineIDSearch(context, id, cacheLocn); SearchCritera critera = new SearchCritera(search, 1, 1, 0); SearchResult result = searchNew(critera, context, progressListener); if(result.getArtists().size() == 1) { id = result.getArtists().get(0).getId(); } else if(result.getAlbums().size() == 1) { id = result.getAlbums().get(0).getId(); } } MusicDirectory dir = null; int index, start = 0; while((index = id.indexOf(';', start)) != -1) { MusicDirectory extra = getMusicDirectoryImpl(id.substring(start, index), name, refresh, context, progressListener); if(dir == null) { dir = extra; } else { dir.addChildren(extra.getChildren()); } start = index + 1; } MusicDirectory extra = getMusicDirectoryImpl(id.substring(start), name, refresh, context, progressListener); if(dir == null) { dir = extra; } else { dir.addChildren(extra.getChildren()); } return dir; } private MusicDirectory getMusicDirectoryImpl(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception { Reader reader = getReader(context, progressListener, "getMusicDirectory", "id", id); try { return new MusicDirectoryParser(context, getInstance(context)).parse(name, reader, progressListener); } finally { Util.close(reader); } } @Override public MusicDirectory getArtist(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception { Reader reader = getReader(context, progressListener, "getArtist", "id", id); try { return new MusicDirectoryParser(context, getInstance(context)).parse(name, reader, progressListener); } finally { Util.close(reader); } } @Override public MusicDirectory getAlbum(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception { Reader reader = getReader(context, progressListener, "getAlbum", "id", id); try { return new MusicDirectoryParser(context, getInstance(context)).parse(name, reader, progressListener); } finally { Util.close(reader); } } @Override public SearchResult search(SearchCritera critera, Context context, ProgressListener progressListener) throws Exception { try { return searchNew(critera, context, progressListener); } catch (ServerTooOldException x) { // Ensure backward compatibility with REST 1.3. return searchOld(critera, context, progressListener); } } /** * Search using the "search" REST method. */ private SearchResult searchOld(SearchCritera critera, Context context, ProgressListener progressListener) throws Exception { List<String> parameterNames = Arrays.asList("any", "songCount"); List<Object> parameterValues = Arrays.<Object>asList(critera.getQuery(), critera.getSongCount()); Reader reader = getReader(context, progressListener, "search", parameterNames, parameterValues); try { return new SearchResultParser(context, getInstance(context)).parse(reader, progressListener); } finally { Util.close(reader); } } /** * Search using the "search2" REST method, available in 1.4.0 and later. */ private SearchResult searchNew(SearchCritera critera, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.4", null); List<String> parameterNames = Arrays.asList("query", "artistCount", "albumCount", "songCount"); List<Object> parameterValues = Arrays.<Object>asList(critera.getQuery(), critera.getArtistCount(), critera.getAlbumCount(), critera.getSongCount()); int instance = getInstance(context); String method; if(ServerInfo.isMadsonic(context, instance) && ServerInfo.checkServerVersion(context, "2.0", instance)) { if(Util.isTagBrowsing(context, instance)) { method = "searchID3"; } else { method = "search"; } } else { if(Util.isTagBrowsing(context, instance)) { method = "search3"; } else { method = "search2"; } } Reader reader = getReader(context, progressListener, method, parameterNames, parameterValues); try { return new SearchResult2Parser(context, getInstance(context)).parse(reader, progressListener); } finally { Util.close(reader); } } @Override public MusicDirectory getPlaylist(boolean refresh, String id, String name, Context context, ProgressListener progressListener) throws Exception { Reader reader = getReader(context, progressListener, "getPlaylist", "id", id, SOCKET_READ_TIMEOUT_GET_PLAYLIST); try { return new PlaylistParser(context, getInstance(context)).parse(reader, progressListener); } finally { Util.close(reader); } } @Override public List<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception { Reader reader = getReader(context, progressListener, "getPlaylists"); try { return new PlaylistsParser(context, getInstance(context)).parse(reader, progressListener); } finally { Util.close(reader); } } @Override public void createPlaylist(String id, String name, List<MusicDirectory.Entry> entries, Context context, ProgressListener progressListener) throws Exception { List<String> parameterNames = new LinkedList<String>(); List<Object> parameterValues = new LinkedList<Object>(); if (id != null) { parameterNames.add("playlistId"); parameterValues.add(id); } if (name != null) { parameterNames.add("name"); parameterValues.add(name); } for (MusicDirectory.Entry entry : entries) { parameterNames.add("songId"); parameterValues.add(getOfflineSongId(entry.getId(), context, progressListener)); } Reader reader = getReader(context, progressListener, "createPlaylist", parameterNames, parameterValues); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { Util.close(reader); } } @Override public void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception { Reader reader = getReader(context, progressListener, "deletePlaylist", "id", id); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { Util.close(reader); } } @Override public void addToPlaylist(String id, List<MusicDirectory.Entry> toAdd, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.8", "Updating playlists is not supported."); List<String> names = new ArrayList<String>(); List<Object> values = new ArrayList<Object>(); names.add("playlistId"); values.add(id); for(MusicDirectory.Entry song: toAdd) { names.add("songIdToAdd"); values.add(getOfflineSongId(song.getId(), context, progressListener)); } Reader reader = getReader(context, progressListener, "updatePlaylist", names, values); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { Util.close(reader); } } @Override public void removeFromPlaylist(String id, List<Integer> toRemove, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.8", "Updating playlists is not supported."); List<String> names = new ArrayList<String>(); List<Object> values = new ArrayList<Object>(); names.add("playlistId"); values.add(id); for(Integer song: toRemove) { names.add("songIndexToRemove"); values.add(song); } Reader reader = getReader(context, progressListener, "updatePlaylist", names, values); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { Util.close(reader); } } @Override public void overwritePlaylist(String id, String name, int toRemove, List<MusicDirectory.Entry> toAdd, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.8", "Updating playlists is not supported."); List<String> names = new ArrayList<String>(); List<Object> values = new ArrayList<Object>(); names.add("playlistId"); values.add(id); names.add("name"); values.add(name); for(MusicDirectory.Entry song: toAdd) { names.add("songIdToAdd"); values.add(song.getId()); } for(int i = 0; i < toRemove; i++) { names.add("songIndexToRemove"); values.add(i); } Reader reader = getReader(context, progressListener, "updatePlaylist", names, values); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { Util.close(reader); } } @Override public void updatePlaylist(String id, String name, String comment, boolean pub, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.8", "Updating playlists is not supported."); Reader reader = getReader(context, progressListener, "updatePlaylist", Arrays.asList("playlistId", "name", "comment", "public"), Arrays.<Object>asList(id, name, comment, pub)); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { Util.close(reader); } } @Override public Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception { Reader reader = getReader(context, progressListener, "getLyrics", Arrays.asList("artist", "title"), Arrays.<Object>asList(artist, title)); try { return new LyricsParser(context, getInstance(context)).parse(reader, progressListener); } finally { Util.close(reader); } } @Override public void scrobble(String id, boolean submission, Context context, ProgressListener progressListener) throws Exception { id = getOfflineSongId(id, context, progressListener); scrobble(id, submission, 0, context, progressListener); } public void scrobble(String id, boolean submission, long time, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.5", "Scrobbling not supported."); Reader reader; if(time > 0){ checkServerVersion(context, "1.8", "Scrobbling with a time not supported."); reader = getReader(context, progressListener, "scrobble", Arrays.asList("id", "submission", "time"), Arrays.<Object>asList(id, submission, time)); } else reader = getReader(context, progressListener, "scrobble", Arrays.asList("id", "submission"), Arrays.<Object>asList(id, submission)); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { Util.close(reader); } } @Override public MusicDirectory getAlbumList(String type, int size, int offset, boolean refresh, Context context, ProgressListener progressListener) throws Exception { List<String> names = new ArrayList<String>(); List<Object> values = new ArrayList<Object>(); names.add("type"); values.add(type); names.add("size"); values.add(size); names.add("offset"); values.add(offset); // Add folder if it was set and is non null int instance = getInstance(context); if(Util.getAlbumListsPerFolder(context, instance)) { String folderId = Util.getSelectedMusicFolderId(context, instance); if(folderId != null) { names.add("musicFolderId"); values.add(folderId); } } String method; if(Util.isTagBrowsing(context, instance)) { if(ServerInfo.isMadsonic6(context, instance)) { method = "getAlbumListID3"; } else { method = "getAlbumList2"; } } else { method = "getAlbumList"; } Reader reader = getReader(context, progressListener, method, names, values, true); try { return new EntryListParser(context, getInstance(context)).parse(reader, progressListener); } finally { Util.close(reader); } } @Override public MusicDirectory getAlbumList(String type, String extra, int size, int offset, boolean refresh, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.10.1", "This type of album list is not supported"); List<String> names = new ArrayList<String>(); List<Object> values = new ArrayList<Object>(); names.add("size"); names.add("offset"); values.add(size); values.add(offset); int instance = getInstance(context); if("genres".equals(type)) { names.add("type"); values.add("byGenre"); names.add("genre"); values.add(extra); } else if("years".equals(type)) { names.add("type"); values.add("byYear"); names.add("fromYear"); names.add("toYear"); int decade = Integer.parseInt(extra); // Reverse chronological order only supported in 5.3+ if(ServerInfo.checkServerVersion(context, "1.13", instance) && !ServerInfo.isMadsonic(context, instance)) { values.add(decade + 9); values.add(decade); } else { values.add(decade); values.add(decade + 9); } } // Add folder if it was set and is non null if(Util.getAlbumListsPerFolder(context, instance)) { String folderId = Util.getSelectedMusicFolderId(context, instance); if(folderId != null) { names.add("musicFolderId"); values.add(folderId); } } String method; if(Util.isTagBrowsing(context, instance)) { if(ServerInfo.isMadsonic6(context, instance)) { method = "getAlbumListID3"; } else { method = "getAlbumList2"; } } else { method = "getAlbumList"; } Reader reader = getReader(context, progressListener, method, names, values, true); try { return new EntryListParser(context, instance).parse(reader, progressListener); } finally { Util.close(reader); } } @Override public MusicDirectory getSongList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception { List<String> names = new ArrayList<String>(); List<Object> values = new ArrayList<Object>(); names.add("size"); values.add(size); names.add("offset"); values.add(offset); String method; switch(type) { case MainFragment.SONGS_NEWEST: method = "getNewaddedSongs"; break; case MainFragment.SONGS_TOP_PLAYED: method = "getTopplayedSongs"; break; case MainFragment.SONGS_RECENT: method = "getLastplayedSongs"; break; case MainFragment.SONGS_FREQUENT: method = "getMostplayedSongs"; break; default: method = "getNewaddedSongs"; } Reader reader = getReader(context, progressListener, method, names, values, true); try { return new EntryListParser(context, getInstance(context)).parse(reader, progressListener); } finally { Util.close(reader); } } @Override public MusicDirectory getRandomSongs(int size, String artistId, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.11", "Artist radio is not supported"); List<String> names = new ArrayList<String>(); List<Object> values = new ArrayList<Object>(); names.add("id"); names.add("count"); values.add(artistId); values.add(size); int instance = getInstance(context); String method; if(ServerInfo.isMadsonic6(context, instance)) { if (Util.isTagBrowsing(context, instance)) { method = "getSimilarSongsID3"; } else { method = "getSimilarSongs"; } } else if(ServerInfo.isMadsonic(context, instance)) { method = "getPandoraSongs"; } else { if (Util.isTagBrowsing(context, instance)) { method = "getSimilarSongs2"; } else { method = "getSimilarSongs"; } } Reader reader = getReader(context, progressListener, method, names, values); try { return new RandomSongsParser(context, instance).parse(reader, progressListener); } finally { Util.close(reader); } } @Override public MusicDirectory getStarredList(Context context, ProgressListener progressListener) throws Exception { List<String> names = new ArrayList<String>(); List<Object> values = new ArrayList<Object>(); // Add folder if it was set and is non null int instance = getInstance(context); if(Util.getAlbumListsPerFolder(context, instance)) { String folderId = Util.getSelectedMusicFolderId(context, instance); if(folderId != null) { names.add("musicFolderId"); values.add(folderId); } } String method; if(Util.isTagBrowsing(context, instance)) { if(ServerInfo.isMadsonic6(context, instance)) { method = "getStarredID3"; } else { method = "getStarred2"; } } else { method = "getStarred"; } Reader reader = getReader(context, progressListener, method, names, values, true); try { return new StarredListParser(context, instance).parse(reader, progressListener); } finally { Util.close(reader); } } @Override public MusicDirectory getRandomSongs(int size, String musicFolderId, String genre, String startYear, String endYear, Context context, ProgressListener progressListener) throws Exception { List<String> names = new ArrayList<String>(); List<Object> values = new ArrayList<Object>(); names.add("size"); values.add(size); if (musicFolderId != null && !"".equals(musicFolderId) && !Util.isTagBrowsing(context, getInstance(context))) { names.add("musicFolderId"); values.add(musicFolderId); } if(genre != null && !"".equals(genre)) { names.add("genre"); values.add(genre); } if(startYear != null && !"".equals(startYear)) { // Check to make sure user isn't doing 2015 -> 2010 since Subsonic will return no results if(endYear != null && !"".equals(endYear)) { try { int startYearInt = Integer.parseInt(startYear); int endYearInt = Integer.parseInt(endYear); if(startYearInt > endYearInt) { String tmp = startYear; startYear = endYear; endYear = tmp; } } catch(Exception e) { Log.w(TAG, "Failed to convert start/end year into ints", e); } } names.add("fromYear"); values.add(startYear); } if(endYear != null && !"".equals(endYear)) { names.add("toYear"); values.add(endYear); } Reader reader = getReader(context, progressListener, "getRandomSongs", names, values); try { return new RandomSongsParser(context, getInstance(context)).parse(reader, progressListener); } finally { Util.close(reader); } } private void checkServerVersion(Context context, String version, String text) throws ServerTooOldException { Version serverVersion = ServerInfo.getServerVersion(context); Version requiredVersion = new Version(version); boolean ok = serverVersion == null || serverVersion.compareTo(requiredVersion) >= 0; if (!ok) { throw new ServerTooOldException(text, serverVersion, requiredVersion); } } @Override public String getCoverArtUrl(Context context, MusicDirectory.Entry entry) throws Exception { StringBuilder builder = new StringBuilder(getRestUrl(context, "getCoverArt")); builder.append("&id=").append(entry.getCoverArt()); String url = builder.toString(); url = Util.replaceInternalUrl(context, url); url = rewriteUrlWithRedirect(context, url); return url; } @Override public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, ProgressListener progressListener, SilentBackgroundTask task) throws Exception { // Synchronize on the entry so that we don't download concurrently for the same song. synchronized (entry) { String url = getRestUrl(context, "getCoverArt"); List<String> parameterNames = Arrays.asList("id"); List<Object> parameterValues = Arrays.<Object>asList(entry.getCoverArt()); return getBitmapFromUrl(context, url, parameterNames, parameterValues, size, FileUtil.getAlbumArtFile(context, entry), true, progressListener, task); } } @Override public HttpURLConnection getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, SilentBackgroundTask task) throws Exception { String url = getRestUrl(context, "stream"); List<String> parameterNames = new ArrayList<String>(); parameterNames.add("id"); parameterNames.add("maxBitRate"); List<Object> parameterValues = new ArrayList<>(); parameterValues.add(song.getId()); parameterValues.add(maxBitrate); // If video specify what format to download if(song.isVideo()) { String videoPlayerType = Util.getVideoPlayerType(context); if("hls".equals(videoPlayerType)) { // HLS should be able to transcode to mp4 automatically parameterNames.add("format"); parameterValues.add("mp4"); parameterNames.add("hls"); parameterValues.add("true"); } else if("raw".equals(videoPlayerType)) { // Download the original video without any transcoding parameterNames.add("format"); parameterValues.add("raw"); } } // Add "Range" header if offset is given Map<String, String> headers = new HashMap<>(); if (offset > 0) { headers.put("Range", "bytes=" + offset + "-"); } // Set socket read timeout. Note: The timeout increases as the offset gets larger. This is // to avoid the thrashing effect seen when offset is combined with transcoding/downsampling on the server. // In that case, the server uses a long time before sending any data, causing the client to time out. int timeout = (int) (SOCKET_READ_TIMEOUT_DOWNLOAD + offset * TIMEOUT_MILLIS_PER_OFFSET_BYTE); HttpURLConnection connection = getConnection(context, url, parameterNames, parameterValues, headers, timeout); // If content type is XML, an error occurred. Get it. String contentType = connection.getContentType(); if (contentType != null && (contentType.startsWith("text/xml") || contentType.startsWith("text/html"))) { InputStream in = getInputStreamFromConnection(connection); try { new ErrorParser(context, getInstance(context)).parse(new InputStreamReader(in, Constants.UTF_8)); } finally { Util.close(in); } } return connection; } @Override public String getMusicUrl(Context context, MusicDirectory.Entry song, int maxBitrate) throws Exception { StringBuilder builder = new StringBuilder(getRestUrl(context, "stream")); builder.append("&id=").append(song.getId()); // Allow user to specify to stream raw formats if available if(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_CAST_STREAM_ORIGINAL, true) && ("mp3".equals(song.getSuffix()) || "flac".equals(song.getSuffix()) || "wav".equals(song.getSuffix()) || "aac".equals(song.getSuffix())) && ServerInfo.checkServerVersion(context, "1.9", getInstance(context))) { builder.append("&format=raw"); } else { builder.append("&maxBitRate=").append(maxBitrate); } String url = builder.toString(); url = Util.replaceInternalUrl(context, url); url = rewriteUrlWithRedirect(context, url); Log.i(TAG, "Using music URL: " + stripUrlInfo(url)); return url; } @Override public String getVideoUrl(int maxBitrate, Context context, String id) { StringBuilder builder = new StringBuilder(getRestUrl(context, "videoPlayer")); builder.append("&id=").append(id); builder.append("&maxBitRate=").append(maxBitrate); builder.append("&autoplay=true"); String url = rewriteUrlWithRedirect(context, builder.toString()); Log.i(TAG, "Using video URL: " + stripUrlInfo(url)); return url; } @Override public String getVideoStreamUrl(String format, int maxBitrate, Context context, String id) throws Exception { StringBuilder builder = new StringBuilder(getRestUrl(context, "stream")); builder.append("&id=").append(id); if(!"raw".equals(format)) { checkServerVersion(context, "1.9", "Video streaming not supported."); builder.append("&maxBitRate=").append(maxBitrate); } builder.append("&format=").append(format); String url = rewriteUrlWithRedirect(context, builder.toString()); Log.i(TAG, "Using video URL: " + stripUrlInfo(url)); return url; } @Override public String getHlsUrl(String id, int bitRate, Context context) throws Exception { checkServerVersion(context, "1.9", "HLS video streaming not supported."); StringBuilder builder = new StringBuilder(getRestUrl(context, "hls")); builder.append("&id=").append(id); if(bitRate > 0) { builder.append("&bitRate=").append(bitRate); } String url = rewriteUrlWithRedirect(context, builder.toString()); Log.i(TAG, "Using hls URL: " + stripUrlInfo(url)); return url; } @Override public RemoteStatus updateJukeboxPlaylist(List<String> ids, Context context, ProgressListener progressListener) throws Exception { int n = ids.size(); List<String> parameterNames = new ArrayList<String>(n + 1); parameterNames.add("action"); for (int i = 0; i < n; i++) { parameterNames.add("id"); } List<Object> parameterValues = new ArrayList<Object>(); parameterValues.add("set"); parameterValues.addAll(ids); return executeJukeboxCommand(context, progressListener, parameterNames, parameterValues); } @Override public RemoteStatus skipJukebox(int index, int offsetSeconds, Context context, ProgressListener progressListener) throws Exception { List<String> parameterNames = Arrays.asList("action", "index", "offset"); List<Object> parameterValues = Arrays.<Object>asList("skip", index, offsetSeconds); return executeJukeboxCommand(context, progressListener, parameterNames, parameterValues); } @Override public RemoteStatus stopJukebox(Context context, ProgressListener progressListener) throws Exception { return executeJukeboxCommand(context, progressListener, Arrays.asList("action"), Arrays.<Object>asList("stop")); } @Override public RemoteStatus startJukebox(Context context, ProgressListener progressListener) throws Exception { return executeJukeboxCommand(context, progressListener, Arrays.asList("action"), Arrays.<Object>asList("start")); } @Override public RemoteStatus getJukeboxStatus(Context context, ProgressListener progressListener) throws Exception { return executeJukeboxCommand(context, progressListener, Arrays.asList("action"), Arrays.<Object>asList("status")); } @Override public RemoteStatus setJukeboxGain(float gain, Context context, ProgressListener progressListener) throws Exception { List<String> parameterNames = Arrays.asList("action", "gain"); List<Object> parameterValues = Arrays.<Object>asList("setGain", gain); return executeJukeboxCommand(context, progressListener, parameterNames, parameterValues); } private RemoteStatus executeJukeboxCommand(Context context, ProgressListener progressListener, List<String> parameterNames, List<Object> parameterValues) throws Exception { checkServerVersion(context, "1.7", "Jukebox not supported."); Reader reader = getReader(context, progressListener, "jukeboxControl", parameterNames, parameterValues); try { return new JukeboxStatusParser(context, getInstance(context)).parse(reader); } finally { Util.close(reader); } } @Override public void setStarred(List<MusicDirectory.Entry> entries, List<MusicDirectory.Entry> artists, List<MusicDirectory.Entry> albums, boolean starred, ProgressListener progressListener, Context context) throws Exception { checkServerVersion(context, "1.8", "Starring is not supported."); List<String> names = new ArrayList<String>(); List<Object> values = new ArrayList<Object>(); if(entries != null && entries.size() > 0) { if(entries.size() > 1) { for (MusicDirectory.Entry entry : entries) { names.add("id"); values.add(entry.getId()); } } else { names.add("id"); values.add(getOfflineSongId(entries.get(0).getId(), context, progressListener)); } } if(artists != null && artists.size() > 0) { for (MusicDirectory.Entry artist : artists) { names.add("artistId"); values.add(artist.getId()); } } if(albums != null && albums.size() > 0) { for (MusicDirectory.Entry album : albums) { names.add("albumId"); values.add(album.getId()); } } Reader reader = getReader(context, progressListener, starred ? "star" : "unstar", names, values); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { Util.close(reader); } } @Override public List<Share> getShares(Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.6", "Shares not supported."); Reader reader = getReader(context, progressListener, "getShares"); try { return new ShareParser(context, getInstance(context)).parse(reader, progressListener); } finally { Util.close(reader); } } @Override public List<Share> createShare(List<String> ids, String description, Long expires, Context context, ProgressListener progressListener) throws Exception { List<String> parameterNames = new LinkedList<String>(); List<Object> parameterValues = new LinkedList<Object>(); for (String id : ids) { parameterNames.add("id"); parameterValues.add(id); } if (description != null) { parameterNames.add("description"); parameterValues.add(description); } if (expires > 0) { parameterNames.add("expires"); parameterValues.add(expires); } Reader reader = getReader(context, progressListener, "createShare", parameterNames, parameterValues); try { return new ShareParser(context, getInstance(context)).parse(reader, progressListener); } finally { Util.close(reader); } } @Override public void deleteShare(String id, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.6", "Shares not supported."); List<String> parameterNames = new ArrayList<String>(); List<Object> parameterValues = new ArrayList<Object>(); parameterNames.add("id"); parameterValues.add(id); Reader reader = getReader(context, progressListener, "deleteShare", parameterNames, parameterValues); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { Util.close(reader); } } @Override public void updateShare(String id, String description, Long expires, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.6", "Updating share not supported."); List<String> parameterNames = new ArrayList<String>(); List<Object> parameterValues = new ArrayList<Object>(); parameterNames.add("id"); parameterValues.add(id); if (description != null) { parameterNames.add("description"); parameterValues.add(description); } parameterNames.add("expires"); parameterValues.add(expires); Reader reader = getReader(context, progressListener, "updateShare", parameterNames, parameterValues); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { Util.close(reader); } } @Override public List<ChatMessage> getChatMessages(Long since, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.2", "Chat not supported."); List<String> parameterNames = new ArrayList<String>(); List<Object> parameterValues = new ArrayList<Object>(); parameterNames.add("since"); parameterValues.add(since); Reader reader = getReader(context, progressListener, "getChatMessages", parameterNames, parameterValues); try { return new ChatMessageParser(context, getInstance(context)).parse(reader, progressListener); } finally { Util.close(reader); } } @Override public void addChatMessage(String message, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.2", "Chat not supported."); List<String> parameterNames = new ArrayList<String>(); List<Object> parameterValues = new ArrayList<Object>(); parameterNames.add("message"); parameterValues.add(message); Reader reader = getReader(context, progressListener, "addChatMessage", parameterNames, parameterValues); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { Util.close(reader); } } @Override public List<Genre> getGenres(boolean refresh, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.9", "Genres not supported."); Reader reader = getReader(context, progressListener, "getGenres"); try { return new GenreParser(context, getInstance(context)).parse(reader, progressListener); } finally { Util.close(reader); } } @Override public MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.9", "Genres not supported."); List<String> parameterNames = new ArrayList<String>(); List<Object> parameterValues = new ArrayList<Object>(); parameterNames.add("genre"); parameterValues.add(genre); parameterNames.add("count"); parameterValues.add(count); parameterNames.add("offset"); parameterValues.add(offset); // Add folder if it was set and is non null int instance = getInstance(context); if(Util.getAlbumListsPerFolder(context, instance)) { String folderId = Util.getSelectedMusicFolderId(context, instance); if(folderId != null) { parameterNames.add("musicFolderId"); parameterValues.add(folderId); } } Reader reader = getReader(context, progressListener, "getSongsByGenre", parameterNames, parameterValues, true); try { return new RandomSongsParser(context, instance).parse(reader, progressListener); } finally { Util.close(reader); } } @Override public MusicDirectory getTopTrackSongs(String artist, int size, Context context, ProgressListener progressListener) throws Exception { List<String> parameterNames = new ArrayList<String>(); List<Object> parameterValues = new ArrayList<Object>(); parameterNames.add("artist"); parameterValues.add(artist); parameterNames.add("size"); parameterValues.add(size); String method = ServerInfo.isMadsonic(context, getInstance(context)) ? "getTopTrackSongs" : "getTopSongs"; Reader reader = getReader(context, progressListener, method, parameterNames, parameterValues); try { return new TopSongsParser(context, getInstance(context)).parse(reader, progressListener); } finally { Util.close(reader); } } @Override public List<PodcastChannel> getPodcastChannels(boolean refresh, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.6", "Podcasts not supported."); Reader reader = getReader(context, progressListener, "getPodcasts", Arrays.asList("includeEpisodes"), Arrays.<Object>asList("false")); try { List<PodcastChannel> channels = new PodcastChannelParser(context, getInstance(context)).parse(reader, progressListener); String content = ""; for(PodcastChannel channel: channels) { content += channel.getName() + "\t" + channel.getUrl() + "\n"; } File file = FileUtil.getPodcastFile(context, Util.getServerName(context, getInstance(context))); BufferedWriter bw = new BufferedWriter(new FileWriter(file)); bw.write(content); bw.close(); return channels; } finally { Util.close(reader); } } @Override public MusicDirectory getPodcastEpisodes(boolean refresh, String id, Context context, ProgressListener progressListener) throws Exception { Reader reader = getReader(context, progressListener, "getPodcasts", Arrays.asList("id"), Arrays.<Object>asList(id)); try { return new PodcastEntryParser(context, getInstance(context)).parse(id, reader, progressListener); } finally { Util.close(reader); } } @Override public MusicDirectory getNewestPodcastEpisodes(boolean refresh, Context context, ProgressListener progressListener, int count) throws Exception { Reader reader = getReader(context, progressListener, "getNewestPodcasts", Arrays.asList("count"), Arrays.<Object>asList(count), true); try { return new PodcastEntryParser(context, getInstance(context)).parse(null, reader, progressListener); } finally { Util.close(reader); } } @Override public void refreshPodcasts(Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.9", "Refresh podcasts not supported."); Reader reader = getReader(context, progressListener, "refreshPodcasts"); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { Util.close(reader); } } @Override public void createPodcastChannel(String url, Context context, ProgressListener progressListener) throws Exception{ checkServerVersion(context, "1.9", "Creating podcasts not supported."); Reader reader = getReader(context, progressListener, "createPodcastChannel", "url", url); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { Util.close(reader); } } @Override public void deletePodcastChannel(String id, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.9", "Deleting podcasts not supported."); Reader reader = getReader(context, progressListener, "deletePodcastChannel", "id", id); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { Util.close(reader); } } @Override public void downloadPodcastEpisode(String id, Context context, ProgressListener progressListener) throws Exception{ checkServerVersion(context, "1.9", "Downloading podcasts not supported."); Reader reader = getReader(context, progressListener, "downloadPodcastEpisode", "id", id); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { Util.close(reader); } } @Override public void deletePodcastEpisode(String id, String parent, ProgressListener progressListener, Context context) throws Exception{ checkServerVersion(context, "1.9", "Deleting podcasts not supported."); Reader reader = getReader(context, progressListener, "deletePodcastEpisode", "id", id); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { Util.close(reader); } } @Override public void setRating(MusicDirectory.Entry entry, int rating, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.6", "Setting ratings not supported."); Reader reader = getReader(context, progressListener, "setRating", Arrays.asList("id", "rating"), Arrays.<Object>asList(entry.getId(), rating)); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { Util.close(reader); } } @Override public MusicDirectory getBookmarks(boolean refresh, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.9", "Bookmarks not supported."); Reader reader = getReader(context, progressListener, "getBookmarks"); try { return new BookmarkParser(context, getInstance(context)).parse(reader, progressListener); } finally { Util.close(reader); } } @Override public void createBookmark(MusicDirectory.Entry entry, int position, String comment, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.9", "Creating bookmarks not supported."); Reader reader = getReader(context, progressListener, "createBookmark", Arrays.asList("id", "position", "comment"), Arrays.<Object>asList(entry.getId(), position, comment)); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { Util.close(reader); } } @Override public void deleteBookmark(MusicDirectory.Entry entry, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.9", "Deleting bookmarks not supported."); Reader reader = getReader(context, progressListener, "deleteBookmark", Arrays.asList("id"), Arrays.<Object>asList(entry.getId())); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { Util.close(reader); } } @Override public User getUser(boolean refresh, String username, Context context, ProgressListener progressListener) throws Exception { Reader reader = getReader(context, progressListener, "getUser", Arrays.asList("username"), Arrays.<Object>asList(username)); try { List<User> users = new UserParser(context, getInstance(context)).parse(reader, progressListener); if(users.size() > 0) { // Should only have returned one anyways return users.get(0); } else { return null; } } finally { Util.close(reader); } } @Override public List<User> getUsers(boolean refresh, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.8", "Getting user list is not supported"); Reader reader = getReader(context, progressListener, "getUsers"); try { return new UserParser(context, getInstance(context)).parse(reader, progressListener); } finally { Util.close(reader); } } @Override public void createUser(User user, Context context, ProgressListener progressListener) throws Exception { List<String> names = new ArrayList<String>(); List<Object> values = new ArrayList<Object>(); names.add("username"); values.add(user.getUsername()); names.add("email"); values.add(user.getEmail()); names.add("password"); values.add(user.getPassword()); for(User.Setting setting: user.getSettings()) { names.add(setting.getName()); values.add(setting.getValue()); } if(user.getMusicFolderSettings() != null) { for(User.Setting setting: user.getMusicFolderSettings()) { if(setting.getValue()) { names.add("musicFolderId"); values.add(setting.getName()); } } } Reader reader = getReader(context, progressListener, "createUser", names, values); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { Util.close(reader); } } @Override public void updateUser(User user, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.10", "Updating user is not supported"); List<String> names = new ArrayList<String>(); List<Object> values = new ArrayList<Object>(); names.add("username"); values.add(user.getUsername()); for(User.Setting setting: user.getSettings()) { if(setting.getName().indexOf("Role") != -1) { names.add(setting.getName()); values.add(setting.getValue()); } } if(user.getMusicFolderSettings() != null) { for(User.Setting setting: user.getMusicFolderSettings()) { if(setting.getValue()) { names.add("musicFolderId"); values.add(setting.getName()); } } } Reader reader = getReader(context, progressListener, "updateUser", names, values); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { Util.close(reader); } } @Override public void deleteUser(String username, Context context, ProgressListener progressListener) throws Exception { Reader reader = getReader(context, progressListener, "deleteUser", Arrays.asList("username"), Arrays.<Object>asList(username)); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { Util.close(reader); } } @Override public void changeEmail(String username, String email, Context context, ProgressListener progressListener) throws Exception { Reader reader = getReader(context, progressListener, "updateUser", Arrays.asList("username", "email"), Arrays.<Object>asList(username, email)); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { Util.close(reader); } } @Override public void changePassword(String username, String password, Context context, ProgressListener progressListener) throws Exception { Reader reader = getReader(context, progressListener, "changePassword", Arrays.asList("username", "password"), Arrays.<Object>asList(username, password)); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { Util.close(reader); } } @Override public Bitmap getAvatar(String username, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception { // Return silently if server is too old if (!ServerInfo.checkServerVersion(context, "1.8")) { return null; } // Synchronize on the username so that we don't download concurrently for // the same user. synchronized (username) { String url = Util.getRestUrl(context, "getAvatar"); List<String> parameterNames = Collections.singletonList("username"); List<Object> parameterValues = Arrays.<Object>asList(username); return getBitmapFromUrl(context, url, parameterNames, parameterValues, size, FileUtil.getAvatarFile(context, username), false, progressListener, task); } } @Override public ArtistInfo getArtistInfo(String id, boolean refresh, boolean allowNetwork, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.11", "Getting artist info is not supported"); int instance = getInstance(context); String method; if(Util.isTagBrowsing(context, instance)) { if(ServerInfo.isMadsonic6(context, instance)) { method = "getArtistInfoID3"; } else { method = "getArtistInfo2"; } } else { method = "getArtistInfo"; } Reader reader = getReader(context, progressListener, method, Arrays.asList("id", "includeNotPresent"), Arrays.<Object>asList(id, "true")); try { return new ArtistInfoParser(context, getInstance(context)).parse(reader, progressListener); } finally { Util.close(reader); } } @Override public Bitmap getBitmap(String url, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception { // Synchronize on the url so that we don't download concurrently synchronized (url) { return getBitmapFromUrl(context, url, null, null, size, FileUtil.getMiscFile(context, url), false, progressListener, task); } } @Override public MusicDirectory getVideos(boolean refresh, Context context, ProgressListener progressListener) throws Exception { Reader reader = getReader(context, progressListener, "getVideos"); try { return new VideosParser(context, getInstance(context)).parse(reader, progressListener); } finally { Util.close(reader); } } @Override public void savePlayQueue(List<MusicDirectory.Entry> songs, MusicDirectory.Entry currentPlaying, int position, Context context, ProgressListener progressListener) throws Exception { List<String> parameterNames = new LinkedList<String>(); List<Object> parameterValues = new LinkedList<Object>(); for(MusicDirectory.Entry song: songs) { parameterNames.add("id"); parameterValues.add(song.getId()); } parameterNames.add("current"); parameterValues.add(currentPlaying.getId()); parameterNames.add("position"); parameterValues.add(position); Reader reader = getReader(context, progressListener, "savePlayQueue", parameterNames, parameterValues); try { new ErrorParser(context, getInstance(context)).parse(reader); } finally { Util.close(reader); } } @Override public PlayerQueue getPlayQueue(Context context, ProgressListener progressListener) throws Exception { Reader reader = getReader(context, progressListener, "getPlayQueue"); try { return new PlayQueueParser(context, getInstance(context)).parse(reader, progressListener); } finally { Util.close(reader); } } @Override public List<InternetRadioStation> getInternetRadioStations(boolean refresh, Context context, ProgressListener progressListener) throws Exception { checkServerVersion(context, "1.9", null); Reader reader = getReader(context, progressListener, "getInternetRadioStations"); try { return new InternetRadioStationParser(context, getInstance(context)).parse(reader, progressListener); } finally { Util.close(reader); } } @Override public int processOfflineSyncs(final Context context, final ProgressListener progressListener) throws Exception{ return processOfflineScrobbles(context, progressListener) + processOfflineStars(context, progressListener); } public int processOfflineScrobbles(final Context context, final ProgressListener progressListener) throws Exception { SharedPreferences offline = Util.getOfflineSync(context); SharedPreferences.Editor offlineEditor = offline.edit(); int count = offline.getInt(Constants.OFFLINE_SCROBBLE_COUNT, 0); int retry = 0; for(int i = 1; i <= count; i++) { try { String id = offline.getString(Constants.OFFLINE_SCROBBLE_ID + i, null); long time = offline.getLong(Constants.OFFLINE_SCROBBLE_TIME + i, 0); if(id != null) { scrobble(id, true, time, context, progressListener); } else { String search = offline.getString(Constants.OFFLINE_SCROBBLE_SEARCH + i, ""); SearchCritera critera = new SearchCritera(search, 0, 0, 1); SearchResult result = searchNew(critera, context, progressListener); if(result.getSongs().size() == 1){ Log.i(TAG, "Query '" + search + "' returned song " + result.getSongs().get(0).getTitle() + " by " + result.getSongs().get(0).getArtist() + " with id " + result.getSongs().get(0).getId()); Log.i(TAG, "Scrobbling " + result.getSongs().get(0).getId() + " with time " + time); scrobble(result.getSongs().get(0).getId(), true, time, context, progressListener); } else{ throw new Exception("Song not found on server"); } } } catch(Exception e){ Log.e(TAG, e.toString()); retry++; } } offlineEditor.putInt(Constants.OFFLINE_SCROBBLE_COUNT, 0); offlineEditor.commit(); return count - retry; } public int processOfflineStars(final Context context, final ProgressListener progressListener) throws Exception { SharedPreferences offline = Util.getOfflineSync(context); SharedPreferences.Editor offlineEditor = offline.edit(); int count = offline.getInt(Constants.OFFLINE_STAR_COUNT, 0); int retry = 0; for(int i = 1; i <= count; i++) { String id = offline.getString(Constants.OFFLINE_STAR_ID + i, null); boolean starred = offline.getBoolean(Constants.OFFLINE_STAR_SETTING + i, false); if(id != null) { setStarred(Arrays.asList(new MusicDirectory.Entry(id)), null, null, starred, progressListener, context); } else { String search = offline.getString(Constants.OFFLINE_STAR_SEARCH + i, ""); try{ SearchCritera critera = new SearchCritera(search, 0, 1, 1); SearchResult result = searchNew(critera, context, progressListener); if(result.getSongs().size() == 1) { MusicDirectory.Entry song = result.getSongs().get(0); Log.i(TAG, "Query '" + search + "' returned song " + song.getTitle() + " by " + song.getArtist() + " with id " + song.getId()); setStarred(Arrays.asList(song), null, null, starred, progressListener, context); } else if(result.getAlbums().size() == 1) { MusicDirectory.Entry album = result.getAlbums().get(0); Log.i(TAG, "Query '" + search + "' returned album " + album.getTitle() + " by " + album.getArtist() + " with id " + album.getId()); if(Util.isTagBrowsing(context, getInstance(context))) { setStarred(null, null, Arrays.asList(album), starred, progressListener, context); } else { setStarred(Arrays.asList(album), null, null, starred, progressListener, context); } } else { throw new Exception("Song not found on server"); } } catch(Exception e) { Log.e(TAG, e.toString()); retry++; } } } offlineEditor.putInt(Constants.OFFLINE_STAR_COUNT, 0); offlineEditor.commit(); return count - retry; } private String getOfflineSongId(String id, Context context, ProgressListener progressListener) throws Exception { SharedPreferences prefs = Util.getPreferences(context); String cacheLocn = prefs.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, null); if(cacheLocn != null && id.indexOf(cacheLocn) != -1) { Pair<Integer, String> cachedSongId = SongDBHandler.getHandler(context).getIdFromPath(Util.getRestUrlHash(context, getInstance(context)), id); if(cachedSongId != null) { id = cachedSongId.getSecond(); } else { String searchCriteria = Util.parseOfflineIDSearch(context, id, cacheLocn); SearchCritera critera = new SearchCritera(searchCriteria, 0, 0, 1); SearchResult result = searchNew(critera, context, progressListener); if (result.getSongs().size() == 1) { id = result.getSongs().get(0).getId(); } } } return id; } @Override public void setInstance(Integer instance) throws Exception { this.instance = instance; } protected Bitmap getBitmapFromUrl(Context context, String url, List<String> parameterNames, List<Object> parameterValues, int size, File saveToFile, boolean allowUnscaled, ProgressListener progressListener, SilentBackgroundTask task) throws Exception { InputStream in = null; try { HttpURLConnection connection = getConnection(context, url, parameterNames, parameterValues, progressListener, true); in = getInputStreamFromConnection(connection); String contentType = connection.getContentType(); if (contentType != null && (contentType.startsWith("text/xml") || contentType.startsWith("text/html"))) { new ErrorParser(context, getInstance(context)).parse(new InputStreamReader(in, Constants.UTF_8)); } byte[] bytes = Util.toByteArray(in); // Handle case where partial was downloaded before being cancelled if(task != null && task.isCancelled()) { return null; } OutputStream out = null; try { out = new FileOutputStream(saveToFile); out.write(bytes); } finally { Util.close(out); } // Size == 0 -> only want to download if(size == 0) { return null; } else { return FileUtil.getSampledBitmap(bytes, size, allowUnscaled); } } finally { Util.close(in); } } // Helper classes to get a reader for the request private Reader getReader(Context context, ProgressListener progressListener, String method) throws Exception { return getReader(context, progressListener, method, (List<String>)null, null); } private Reader getReader(Context context, ProgressListener progressListener, String method, String parameterName, Object parameterValue) throws Exception { return getReader(context, progressListener, method, parameterName, parameterValue, 0); } private Reader getReader(Context context, ProgressListener progressListener, String method, String parameterName, Object parameterValue, int minNetworkTimeout) throws Exception { return getReader(context, progressListener, method, Arrays.asList(parameterName), Arrays.asList(parameterValue), minNetworkTimeout); } private Reader getReader(Context context, ProgressListener progressListener, String method, List<String> parameterNames, List<Object> parameterValues) throws Exception { return getReader(context, progressListener, method, parameterNames, parameterValues, 0); } private Reader getReader(Context context, ProgressListener progressListener, String method, List<String> parameterNames, List<Object> parameterValues, int minNetworkTimeout) throws Exception { return getReader(context, progressListener, method, parameterNames, parameterValues, minNetworkTimeout, false); } private Reader getReader(Context context, ProgressListener progressListener, String method, List<String> parameterNames, List<Object> parameterValues, boolean throwErrors) throws Exception { return getReader(context, progressListener, method, parameterNames, parameterValues, 0, throwErrors); } private Reader getReader(Context context, ProgressListener progressListener, String method, List<String> parameterNames, List<Object> parameterValues, int minNetworkTimeout, boolean throwErrors) throws Exception { if (progressListener != null) { progressListener.updateProgress(R.string.service_connecting); } String url = getRestUrl(context, method); return getReaderForURL(context, url, parameterNames, parameterValues, minNetworkTimeout, progressListener, throwErrors); } private Reader getReaderForURL(Context context, String url, List<String> parameterNames, List<Object> parameterValues, ProgressListener progressListener) throws Exception { return getReaderForURL(context, url, parameterNames, parameterValues, progressListener, true); } private Reader getReaderForURL(Context context, String url, List<String> parameterNames, List<Object> parameterValues, ProgressListener progressListener, boolean throwErrors) throws Exception { return getReaderForURL(context, url, parameterNames, parameterValues, 0, progressListener, throwErrors); } private Reader getReaderForURL(Context context, String url, List<String> parameterNames, List<Object> parameterValues, int minNetworkTimeout, ProgressListener progressListener, boolean throwErrors) throws Exception { InputStream in = getInputStream(context, url, parameterNames, parameterValues, minNetworkTimeout, progressListener, throwErrors); return new InputStreamReader(in, Constants.UTF_8); } // Helper classes to open a connection to a server private InputStream getInputStream(Context context, String url, List<String> parameterNames, List<Object> parameterValues, ProgressListener progressListener, boolean throwsErrors) throws Exception { return getInputStream(context, url, parameterNames, parameterValues, 0, progressListener, throwsErrors); } private InputStream getInputStream(Context context, String url, List<String> parameterNames, List<Object> parameterValues, int minNetworkTimeout, ProgressListener progressListener, boolean throwsErrors) throws Exception { HttpURLConnection connection = getConnection(context, url, parameterNames, parameterValues, minNetworkTimeout, progressListener, throwsErrors); return getInputStreamFromConnection(connection); } private InputStream getInputStreamFromConnection(HttpURLConnection connection) throws Exception { InputStream in = connection.getInputStream(); if("gzip".equals(connection.getContentEncoding())) { in = new GZIPInputStream(in); } return in; } private HttpURLConnection getConnection(Context context, String url, List<String> parameterNames, List<Object> parameterValues, Map<String, String> headers, int minNetworkTimeout) throws Exception { return getConnection(context, url, parameterNames, parameterValues, headers, minNetworkTimeout, null, true); } private HttpURLConnection getConnection(Context context, String url, List<String> parameterNames, List<Object> parameterValues, ProgressListener progressListener, boolean throwErrors) throws Exception { return getConnection(context, url, parameterNames, parameterValues, 0, progressListener, throwErrors); } private HttpURLConnection getConnection(Context context, String url, List<String> parameterNames, List<Object> parameterValues, int minNetworkTimeout, ProgressListener progressListener, boolean throwErrors) throws Exception { return getConnection(context, url, parameterNames, parameterValues, null, minNetworkTimeout, progressListener, throwErrors); } private HttpURLConnection getConnection(Context context, String url, List<String> parameterNames, List<Object> parameterValues, Map<String, String> headers, int minNetworkTimeout, ProgressListener progressListener, boolean throwErrors) throws Exception { if(throwErrors) { SharedPreferences prefs = Util.getPreferences(context); int networkTimeout = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT, SOCKET_READ_TIMEOUT_DEFAULT + "")); return getConnectionDirect(context, url, parameterNames, parameterValues, headers, Math.max(minNetworkTimeout, networkTimeout)); } else { return getConnection(context, url, parameterNames, parameterValues, headers, minNetworkTimeout, progressListener, HTTP_REQUEST_MAX_ATTEMPTS, 0); } } private HttpURLConnection getConnection(Context context, String url, List<String> parameterNames, List<Object> parameterValues, Map<String, String> headers, int minNetworkTimeout, ProgressListener progressListener, int retriesLeft, int attempts) throws Exception { SharedPreferences prefs = Util.getPreferences(context); int networkTimeout = Integer.parseInt(prefs.getString(Constants.PREFERENCES_KEY_NETWORK_TIMEOUT, SOCKET_READ_TIMEOUT_DEFAULT + "")); minNetworkTimeout = Math.max(minNetworkTimeout, networkTimeout); attempts++; retriesLeft--; try { return getConnectionDirect(context, url, parameterNames, parameterValues, headers, minNetworkTimeout); } catch (IOException x) { if(retriesLeft > 0) { if (progressListener != null) { String msg = context.getResources().getString(R.string.music_service_retry, attempts, HTTP_REQUEST_MAX_ATTEMPTS - 1); progressListener.updateProgress(msg); } Log.w(TAG, "Got IOException " + x + " (" + attempts + "), will retry"); Thread.sleep(2000L); minNetworkTimeout = (int) (minNetworkTimeout * 1.3); return getConnection(context, url, parameterNames, parameterValues, headers, minNetworkTimeout, progressListener, retriesLeft, attempts); } else { throw x; } } } private HttpURLConnection getConnectionDirect(Context context, String url, List<String> parameterNames, List<Object> parameterValues, Map<String, String> headers, int minNetworkTimeout) throws Exception { // Add params to query if (parameterNames != null) { StringBuilder builder = new StringBuilder(url); for (int i = 0; i < parameterNames.size(); i++) { builder.append("&").append(parameterNames.get(i)).append("="); String part = URLEncoder.encode(String.valueOf(parameterValues.get(i)), "UTF-8"); part = part.replaceAll("\\%27", "'"); builder.append(part); } url = builder.toString(); } // Rewrite url based on redirects String rewrittenUrl = rewriteUrlWithRedirect(context, url); if(rewrittenUrl.indexOf("scanstatus") == -1) { Log.i(TAG, stripUrlInfo(rewrittenUrl)); } return getConnectionDirect(context, rewrittenUrl, headers, minNetworkTimeout); } private HttpURLConnection getConnectionDirect(Context context, String url, Map<String, String> headers, int minNetworkTimeout) throws Exception { if(!hasInstalledGoogleSSL) { try { ProviderInstaller.installIfNeeded(context); } catch(Exception e) { // Just continue on anyways, doesn't really harm anything if this fails Log.w(TAG, "Failed to update to use Google Play SSL", e); } hasInstalledGoogleSSL = true; } // Connect and add headers URL urlObj = new URL(url); HttpURLConnection connection = (HttpURLConnection) urlObj.openConnection(); if(url.indexOf("getCoverArt") == -1 && url.indexOf("stream") == -1 && url.indexOf("getAvatar") == -1) { connection.addRequestProperty("Accept-Encoding", "gzip"); } connection.addRequestProperty("User-Agent", Constants.REST_CLIENT_ID); // Set timeout connection.setConnectTimeout(minNetworkTimeout); connection.setReadTimeout(minNetworkTimeout); // Add headers if(headers != null) { for(Map.Entry<String, String> header: headers.entrySet()) { connection.setRequestProperty(header.getKey(), header.getValue()); } } if(connection instanceof HttpsURLConnection) { HttpsURLConnection sslConnection = (HttpsURLConnection) connection; sslConnection.setSSLSocketFactory(sslSocketFactory); sslConnection.setHostnameVerifier(selfSignedHostnameVerifier); } SharedPreferences prefs = Util.getPreferences(context); int instance = getInstance(context); String username = prefs.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null); String password = prefs.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null); String encoded = Base64.encodeToString((username + ":" + password).getBytes("UTF-8"), Base64.NO_WRAP);; connection.setRequestProperty("Authorization", "Basic " + encoded); // Force the connection to initiate if(connection.getResponseCode() >= 500) { throw new IOException("Error code: " + connection.getResponseCode()); } if(detectRedirect(context, urlObj, connection)) { String rewrittenUrl = rewriteUrlWithRedirect(context, url); if(!rewrittenUrl.equals(url)) { connection.disconnect(); return getConnectionDirect(context, rewrittenUrl, headers, minNetworkTimeout); } } return connection; } // Returns true when we should immediately retry with the redirect private boolean detectRedirect(Context context, URL originalUrl, HttpURLConnection connection) throws Exception { if(connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_TEMP || connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_PERM) { String redirectLocation = connection.getHeaderField("Location"); if(redirectLocation != null) { detectRedirect(context, originalUrl.toExternalForm(), redirectLocation); return true; } } detectRedirect(context, originalUrl, connection.getURL()); return false; } private void detectRedirect(Context context, URL originalUrl, URL redirectedUrl) throws Exception { detectRedirect(context, originalUrl.toExternalForm(), redirectedUrl.toExternalForm()); } private void detectRedirect(Context context, String originalUrl, String redirectedUrl) throws Exception { if(redirectedUrl != null && "http://subsonic.org/pages/".equals(redirectedUrl)) { throw new Exception("Invalid url, redirects to http://subsonic.org/pages/"); } int fromIndex = originalUrl.indexOf("/rest/"); int toIndex = redirectedUrl.indexOf("/rest/"); if(fromIndex != -1 && toIndex != -1 && !Util.equals(originalUrl, redirectedUrl)) { redirectFrom = originalUrl.substring(0, fromIndex); redirectTo = redirectedUrl.substring(0, toIndex); if (redirectFrom.compareTo(redirectTo) != 0) { Log.i(TAG, redirectFrom + " redirects to " + redirectTo); } redirectionLastChecked = System.currentTimeMillis(); redirectionNetworkType = getCurrentNetworkType(context); } } private String rewriteUrlWithRedirect(Context context, String url) { // Only cache for a certain time. if (System.currentTimeMillis() - redirectionLastChecked > REDIRECTION_CHECK_INTERVAL_MILLIS) { return url; } // Ignore cache if network type has changed. if (redirectionNetworkType != getCurrentNetworkType(context)) { return url; } if (redirectFrom == null || redirectTo == null) { return url; } return url.replace(redirectFrom, redirectTo); } private String stripUrlInfo(String url) { return url.substring(0, url.indexOf("?u=") + 1) + url.substring(url.indexOf("&v=") + 1); } private int getCurrentNetworkType(Context context) { ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = manager.getActiveNetworkInfo(); return networkInfo == null ? -1 : networkInfo.getType(); } public int getInstance(Context context) { if(instance == null) { return Util.getActiveServer(context); } else { return instance; } } public String getRestUrl(Context context, String method) { return getRestUrl(context, method, true); } public String getRestUrl(Context context, String method, boolean allowAltAddress) { if(instance == null) { return Util.getRestUrl(context, method, allowAltAddress); } else { return Util.getRestUrl(context, method, instance, allowAltAddress); } } public SSLSocketFactory getSSLSocketFactory() { return sslSocketFactory; } public HostnameVerifier getHostNameVerifier() { return selfSignedHostnameVerifier; } }