/*
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 github.daneren2005.dsub.service;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.concurrent.TimeUnit;
import org.apache.http.HttpResponse;
import android.content.Context;
import android.graphics.Bitmap;
import android.util.Log;
import github.daneren2005.dsub.domain.Artist;
import github.daneren2005.dsub.domain.ArtistInfo;
import github.daneren2005.dsub.domain.Bookmark;
import github.daneren2005.dsub.domain.ChatMessage;
import github.daneren2005.dsub.domain.Genre;
import github.daneren2005.dsub.domain.Indexes;
import github.daneren2005.dsub.domain.PlayerQueue;
import github.daneren2005.dsub.domain.PodcastEpisode;
import github.daneren2005.dsub.domain.RemoteStatus;
import github.daneren2005.dsub.domain.Lyrics;
import github.daneren2005.dsub.domain.MusicDirectory;
import github.daneren2005.dsub.domain.MusicFolder;
import github.daneren2005.dsub.domain.Playlist;
import github.daneren2005.dsub.domain.PodcastChannel;
import github.daneren2005.dsub.domain.SearchCritera;
import github.daneren2005.dsub.domain.SearchResult;
import github.daneren2005.dsub.domain.Share;
import github.daneren2005.dsub.domain.User;
import github.daneren2005.dsub.util.SilentBackgroundTask;
import github.daneren2005.dsub.util.ProgressListener;
import github.daneren2005.dsub.util.SongDBHandler;
import github.daneren2005.dsub.util.SyncUtil;
import github.daneren2005.dsub.util.TimeLimitedCache;
import github.daneren2005.dsub.util.FileUtil;
import github.daneren2005.dsub.util.Util;
import static github.daneren2005.dsub.domain.MusicDirectory.Entry;
/**
* @author Sindre Mehus
*/
public class CachedMusicService implements MusicService {
private static final String TAG = CachedMusicService.class.getSimpleName();
private static final int MUSIC_DIR_CACHE_SIZE = 20;
private static final int TTL_MUSIC_DIR = 5 * 60; // Five minutes
private final RESTMusicService musicService;
private final TimeLimitedCache cachedLicenseValid = new TimeLimitedCache(120, TimeUnit.SECONDS);
private final TimeLimitedCache cachedIndexes = new TimeLimitedCache(60 * 60, TimeUnit.SECONDS);
private final TimeLimitedCache> cachedPlaylists = new TimeLimitedCache>(3600, TimeUnit.SECONDS);
private final TimeLimitedCache> cachedMusicFolders = new TimeLimitedCache>(10 * 3600, TimeUnit.SECONDS);
private final TimeLimitedCache> cachedPodcastChannels = new TimeLimitedCache>(10 * 3600, TimeUnit.SECONDS);
private String restUrl;
private String musicFolderId;
private boolean isTagBrowsing = false;
public CachedMusicService(RESTMusicService musicService) {
this.musicService = musicService;
}
@Override
public void ping(Context context, ProgressListener progressListener) throws Exception {
checkSettingsChanged(context);
musicService.ping(context, progressListener);
}
@Override
public boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception {
checkSettingsChanged(context);
Boolean result = cachedLicenseValid.get();
if (result == null) {
result = FileUtil.deserialize(context, getCacheName(context, "license"), Boolean.class);
if(result == null) {
result = musicService.isLicenseValid(context, progressListener);
// Only save a copy license is valid
if(result) {
FileUtil.serialize(context, (Boolean) result, getCacheName(context, "license"));
}
}
cachedLicenseValid.set(result, result ? 30L * 60L : 2L * 60L, TimeUnit.SECONDS);
}
return result;
}
@Override
public List getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
checkSettingsChanged(context);
if (refresh) {
cachedMusicFolders.clear();
}
List result = cachedMusicFolders.get();
if (result == null) {
if(!refresh) {
result = FileUtil.deserialize(context, getCacheName(context, "musicFolders"), ArrayList.class);
}
if(result == null) {
result = musicService.getMusicFolders(refresh, context, progressListener);
FileUtil.serialize(context, new ArrayList(result), getCacheName(context, "musicFolders"));
}
cachedMusicFolders.set(result);
}
return result;
}
@Override
public void startRescan(Context context, ProgressListener listener) throws Exception {
musicService.startRescan(context, listener);
}
@Override
public Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
checkSettingsChanged(context);
if (refresh) {
cachedIndexes.clear();
cachedMusicFolders.clear();
}
Indexes result = cachedIndexes.get();
if (result == null) {
String name = Util.isTagBrowsing(context, musicService.getInstance(context)) ? "artists" : "indexes";
name = getCacheName(context, name, musicFolderId);
if(!refresh) {
result = FileUtil.deserialize(context, name, Indexes.class);
}
if(result == null) {
result = musicService.getIndexes(musicFolderId, refresh, context, progressListener);
FileUtil.serialize(context, result, name);
}
cachedIndexes.set(result);
}
return result;
}
@Override
public MusicDirectory getMusicDirectory(final String id, final String name, final boolean refresh, final Context context, final ProgressListener progressListener) throws Exception {
MusicDirectory dir = null;
final MusicDirectory cached = FileUtil.deserialize(context, getCacheName(context, "directory", id), MusicDirectory.class);
if(!refresh && cached != null) {
dir = cached;
new SilentBackgroundTask(context) {
@Override
protected Void doInBackground() throws Throwable {
Util.sleepQuietly(2000L);
MusicDirectory refreshed = musicService.getMusicDirectory(id, name, true, context, null);
updateAllSongs(context, refreshed);
cached.updateDifferences(context, musicService.getInstance(context), refreshed);
FileUtil.serialize(context, refreshed, getCacheName(context, "directory", id));
return null;
}
// TODO: When upgrading to RecyclerView, this should be usable since won't have split entry/album lists
/*@Override
public void done(Void result) {
if(progressListener != null) {
progressListener.updateCache();
}
}*/
@Override
public void error(Throwable error) {
Log.e(TAG, "Failed to refresh music directory", error);
}
}.execute();
}
if(dir == null) {
dir = musicService.getMusicDirectory(id, name, refresh, context, progressListener);
updateAllSongs(context, dir);
FileUtil.serialize(context, dir, getCacheName(context, "directory", id));
// If a cached copy exists to check against, look for removes
deleteRemovedEntries(context, dir, cached);
}
return dir;
}
@Override
public MusicDirectory getArtist(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
MusicDirectory dir = null;
MusicDirectory cached = FileUtil.deserialize(context, getCacheName(context, "artist", id), MusicDirectory.class);
if(!refresh) {
dir = cached;
}
if(dir == null) {
dir = musicService.getArtist(id, name, refresh, context, progressListener);
FileUtil.serialize(context, dir, getCacheName(context, "artist", id));
// If a cached copy exists to check against, look for removes
deleteRemovedEntries(context, dir, cached);
}
return dir;
}
@Override
public MusicDirectory getAlbum(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
MusicDirectory dir = null;
MusicDirectory cached = FileUtil.deserialize(context, getCacheName(context, "album", id), MusicDirectory.class);
if(!refresh) {
dir = cached;
}
if(dir == null) {
dir = musicService.getAlbum(id, name, refresh, context, progressListener);
updateAllSongs(context, dir);
FileUtil.serialize(context, dir, getCacheName(context, "album", id));
// If a cached copy exists to check against, look for removes
deleteRemovedEntries(context, dir, cached);
}
return dir;
}
@Override
public SearchResult search(SearchCritera criteria, Context context, ProgressListener progressListener) throws Exception {
return musicService.search(criteria, context, progressListener);
}
@Override
public MusicDirectory getPlaylist(boolean refresh, String id, String name, Context context, ProgressListener progressListener) throws Exception {
MusicDirectory dir = null;
MusicDirectory cachedPlaylist = FileUtil.deserialize(context, getCacheName(context, "playlist", id), MusicDirectory.class);
if(!refresh) {
dir = cachedPlaylist;
}
if(dir == null) {
dir = musicService.getPlaylist(refresh, id, name, context, progressListener);
updateAllSongs(context, dir);
FileUtil.serialize(context, dir, getCacheName(context, "playlist", id));
File playlistFile = FileUtil.getPlaylistFile(context, Util.getServerName(context, musicService.getInstance(context)), dir.getName());
if(cachedPlaylist == null || !playlistFile.exists() || !cachedPlaylist.getChildren().equals(dir.getChildren())) {
FileUtil.writePlaylistFile(context, playlistFile, dir);
}
if(cachedPlaylist != null) {
// Make sure this playlist is supposed to be synced
ArrayList playlistList = SyncUtil.getSyncedPlaylists(context, musicService.getInstance(context));
for(int i = 0; i < playlistList.size(); i++) {
SyncUtil.SyncSet syncPlaylist = playlistList.get(i);
if(syncPlaylist.id != null && syncPlaylist.id.equals(id)) {
List toDelete = cachedPlaylist.getChildren();
for (Entry entry : dir.getChildren()) {
toDelete.remove(entry);
}
for (Entry entry : toDelete) {
DownloadFile file = new DownloadFile(context, entry, true);
file.unpin();
}
break;
}
}
}
}
return dir;
}
@Override
public List getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
checkSettingsChanged(context);
List result = refresh ? null : cachedPlaylists.get();
if (result == null) {
if(!refresh) {
result = FileUtil.deserialize(context, getCacheName(context, "playlist"), ArrayList.class);
}
if(result == null) {
result = musicService.getPlaylists(refresh, context, progressListener);
FileUtil.serialize(context, new ArrayList(result), getCacheName(context, "playlist"));
}
cachedPlaylists.set(result);
}
return result;
}
@Override
public void createPlaylist(String id, String name, List entries, Context context, ProgressListener progressListener) throws Exception {
cachedPlaylists.clear();
Util.delete(new File(context.getCacheDir(), getCacheName(context, "playlist")));
musicService.createPlaylist(id, name, entries, context, progressListener);
}
@Override
public void deletePlaylist(final String id, Context context, ProgressListener progressListener) throws Exception {
musicService.deletePlaylist(id, context, progressListener);
new PlaylistUpdater(context, id) {
@Override
public void updateResult(List objects, Playlist result) {
objects.remove(result);
cachedPlaylists.set(objects);
ArrayList playlistList = SyncUtil.getSyncedPlaylists(context, musicService.getInstance(context));
for(int i = 0; i < playlistList.size(); i++) {
SyncUtil.SyncSet syncPlaylist = playlistList.get(i);
if(syncPlaylist.id != null && syncPlaylist.id.equals(id)) {
MusicDirectory musicDirectory = FileUtil.deserialize(context, getCacheName(context, "playlist", id), MusicDirectory.class);
for(Entry entry: musicDirectory.getChildren()) {
DownloadFile file = new DownloadFile(context, entry, true);
file.unpin();
}
break;
}
}
}
}.execute();
}
@Override
public void addToPlaylist(String id, final List toAdd, Context context, ProgressListener progressListener) throws Exception {
musicService.addToPlaylist(id, toAdd, context, progressListener);
new MusicDirectoryUpdater(context, "playlist", id) {
@Override
public boolean checkResult(Entry check) {
return true;
}
@Override
public void updateResult(List objects, Entry result) {
objects.addAll(toAdd);
}
}.execute();
}
@Override
public void removeFromPlaylist(final String id, final List toRemove, Context context, ProgressListener progressListener) throws Exception {
musicService.removeFromPlaylist(id, toRemove, context, progressListener);
new MusicDirectoryUpdater(context, "playlist", id) {
@Override
public boolean checkResult(Entry check) {
return true;
}
@Override
public void updateResult(List objects, Entry result) {
// Make sure this playlist is supposed to be synced
boolean supposedToUnpin = false;
ArrayList playlistList = SyncUtil.getSyncedPlaylists(context, musicService.getInstance(context));
for(int i = 0; i < playlistList.size(); i++) {
SyncUtil.SyncSet syncPlaylist = playlistList.get(i);
if(syncPlaylist.id != null && syncPlaylist.id.equals(id)) {
supposedToUnpin = true;
break;
}
}
// Remove in reverse order so indexes are still correct as we iterate through
for(ListIterator iterator = toRemove.listIterator(toRemove.size()); iterator.hasPrevious(); ) {
int index = iterator.previous();
if(supposedToUnpin) {
Entry entry = objects.get(index);
DownloadFile file = new DownloadFile(context, entry, true);
file.unpin();
}
objects.remove(index);
}
}
}.execute();
}
@Override
public void overwritePlaylist(String id, String name, int toRemove, final List toAdd, Context context, ProgressListener progressListener) throws Exception {
musicService.overwritePlaylist(id, name, toRemove, toAdd, context, progressListener);
new MusicDirectoryUpdater(context, "playlist", id) {
@Override
public boolean checkResult(Entry check) {
return true;
}
@Override
public void updateResult(List objects, Entry result) {
objects.clear();
objects.addAll(toAdd);
}
}.execute();
}
@Override
public void updatePlaylist(String id, final String name, final String comment, final boolean pub, Context context, ProgressListener progressListener) throws Exception {
musicService.updatePlaylist(id, name, comment, pub, context, progressListener);
new PlaylistUpdater(context, id) {
@Override
public void updateResult(List objects, Playlist result) {
result.setName(name);
result.setComment(comment);
result.setPublic(pub);
cachedPlaylists.set(objects);
}
}.execute();
}
@Override
public Lyrics getLyrics(String artist, String title, Context context, ProgressListener progressListener) throws Exception {
return musicService.getLyrics(artist, title, context, progressListener);
}
@Override
public void scrobble(String id, boolean submission, Context context, ProgressListener progressListener) throws Exception {
musicService.scrobble(id, submission, context, progressListener);
}
@Override
public MusicDirectory getAlbumList(String type, int size, int offset, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
try {
MusicDirectory dir = musicService.getAlbumList(type, size, offset, refresh, context, progressListener);
// Do some serialization updates for changes to recently added
if ("newest".equals(type) && offset == 0) {
String recentlyAddedFile = getCacheName(context, type);
ArrayList recents = FileUtil.deserialize(context, recentlyAddedFile, ArrayList.class);
if (recents == null) {
recents = new ArrayList();
}
// Add any new items
final int instance = musicService.getInstance(context);
isTagBrowsing = Util.isTagBrowsing(context, instance);
for (final Entry album : dir.getChildren()) {
if (!recents.contains(album.getId())) {
recents.add(album.getId());
String cacheName, parent;
if (isTagBrowsing) {
cacheName = "artist";
parent = album.getArtistId();
} else {
cacheName = "directory";
parent = album.getParent();
}
// Add album to artist
if (parent != null) {
new MusicDirectoryUpdater(context, cacheName, parent) {
private boolean changed = false;
@Override
public boolean checkResult(Entry check) {
return true;
}
@Override
public void updateResult(List objects, Entry result) {
// Only add if it doesn't already exist in it!
if (!objects.contains(album)) {
objects.add(album);
changed = true;
}
}
@Override
public void save(ArrayList objects) {
// Only save if actually added to artist
if (changed) {
musicDirectory.replaceChildren(objects);
// Reapply sort after addition
musicDirectory.sortChildren(context, instance);
FileUtil.serialize(context, musicDirectory, cacheName);
}
}
}.execute();
} else {
// If parent is null, then this is a root level album
final Artist artist = new Artist();
artist.setId(album.getId());
artist.setName(album.getTitle());
new IndexesUpdater(context, isTagBrowsing ? "artists" : "indexes") {
private boolean changed = false;
@Override
public boolean checkResult(Artist check) {
return true;
}
@Override
public void updateResult(List objects, Artist result) {
if (!objects.contains(artist)) {
objects.add(artist);
changed = true;
}
}
@Override
public void save(ArrayList objects) {
if (changed) {
indexes.setArtists(objects);
// Reapply sort after addition
indexes.sortChildren(context);
FileUtil.serialize(context, indexes, cacheName);
cachedIndexes.set(indexes);
}
}
}.execute();
}
}
}
// Keep list from growing into infinity
while (recents.size() > 0) {
recents.remove(0);
}
FileUtil.serialize(context, recents, recentlyAddedFile);
}
FileUtil.serialize(context, dir, getCacheName(context, type, Integer.toString(offset)));
return dir;
} catch(IOException e) {
Log.w(TAG, "Failed to refresh album list: ", e);
if(refresh) {
throw e;
}
MusicDirectory dir = FileUtil.deserialize(context, getCacheName(context, type, Integer.toString(offset)), MusicDirectory.class);
if(dir == null) {
// If we are at start and no cache, throw error higher
if(offset == 0) {
throw e;
} else {
// Otherwise just pretend we are at the end of the list
return new MusicDirectory();
}
} else {
return dir;
}
}
}
@Override
public MusicDirectory getAlbumList(String type, String extra, int size, int offset, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
try {
MusicDirectory dir = musicService.getAlbumList(type, extra, size, offset, refresh, context, progressListener);
FileUtil.serialize(context, dir, getCacheName(context, type + extra, Integer.toString(offset)));
return dir;
} catch(IOException e) {
Log.w(TAG, "Failed to refresh album list: ", e);
if(refresh) {
throw e;
}
MusicDirectory dir = FileUtil.deserialize(context, getCacheName(context, type + extra, Integer.toString(offset)), MusicDirectory.class);
if(dir == null) {
// If we are at start and no cache, throw error higher
if(offset == 0) {
throw e;
} else {
// Otherwise just pretend we are at the end of the list
return new MusicDirectory();
}
} else {
return dir;
}
}
}
@Override
public MusicDirectory getRandomSongs(int size, String artistId, Context context, ProgressListener progressListener) throws Exception {
return musicService.getRandomSongs(size, artistId, context, progressListener);
}
@Override
public MusicDirectory getStarredList(Context context, ProgressListener progressListener) throws Exception {
try {
MusicDirectory dir = musicService.getStarredList(context, progressListener);
MusicDirectory oldDir = FileUtil.deserialize(context, "starred", MusicDirectory.class);
if (oldDir != null) {
final List newList = new ArrayList();
newList.addAll(dir.getChildren());
final List oldList = oldDir.getChildren();
for (Iterator it = oldList.iterator(); it.hasNext(); ) {
Entry oldEntry = it.next();
// Remove entries from newList
if (newList.remove(oldEntry)) {
// If it was removed, then remove it from old list as well
it.remove();
} else {
oldEntry.setStarred(false);
}
}
List totalList = new ArrayList();
totalList.addAll(oldList);
totalList.addAll(newList);
new StarUpdater(context, totalList).execute();
}
FileUtil.serialize(context, dir, "starred");
return dir;
} catch(IOException e) {
MusicDirectory dir = FileUtil.deserialize(context, "starred", MusicDirectory.class);
if(dir == null) {
throw e;
} else {
return dir;
}
}
}
@Override
public MusicDirectory getRandomSongs(int size, String folder, String genre, String startYear, String endYear, Context context, ProgressListener progressListener) throws Exception {
return musicService.getRandomSongs(size, folder, genre, startYear, endYear, context, progressListener);
}
@Override
public String getCoverArtUrl(Context context, Entry entry) throws Exception {
return musicService.getCoverArtUrl(context, entry);
}
@Override
public Bitmap getCoverArt(Context context, Entry entry, int size, ProgressListener progressListener, SilentBackgroundTask task) throws Exception {
return musicService.getCoverArt(context, entry, size, progressListener, task);
}
@Override
public HttpResponse getDownloadInputStream(Context context, Entry song, long offset, int maxBitrate, SilentBackgroundTask task) throws Exception {
return musicService.getDownloadInputStream(context, song, offset, maxBitrate, task);
}
@Override
public String getMusicUrl(Context context, Entry song, int maxBitrate) throws Exception {
return musicService.getMusicUrl(context, song, maxBitrate);
}
@Override
public String getVideoUrl(int maxBitrate, Context context, String id) {
return musicService.getVideoUrl(maxBitrate, context, id);
}
@Override
public String getVideoStreamUrl(String format, int maxBitrate, Context context, String id) throws Exception {
return musicService.getVideoStreamUrl(format, maxBitrate, context, id);
}
@Override
public String getHlsUrl(String id, int bitRate, Context context) throws Exception {
return musicService.getHlsUrl(id, bitRate, context);
}
@Override
public RemoteStatus updateJukeboxPlaylist(List ids, Context context, ProgressListener progressListener) throws Exception {
return musicService.updateJukeboxPlaylist(ids, context, progressListener);
}
@Override
public RemoteStatus skipJukebox(int index, int offsetSeconds, Context context, ProgressListener progressListener) throws Exception {
return musicService.skipJukebox(index, offsetSeconds, context, progressListener);
}
@Override
public RemoteStatus stopJukebox(Context context, ProgressListener progressListener) throws Exception {
return musicService.stopJukebox(context, progressListener);
}
@Override
public RemoteStatus startJukebox(Context context, ProgressListener progressListener) throws Exception {
return musicService.startJukebox(context, progressListener);
}
@Override
public RemoteStatus getJukeboxStatus(Context context, ProgressListener progressListener) throws Exception {
return musicService.getJukeboxStatus(context, progressListener);
}
@Override
public RemoteStatus setJukeboxGain(float gain, Context context, ProgressListener progressListener) throws Exception {
return musicService.setJukeboxGain(gain, context, progressListener);
}
@Override
public void setStarred(List entries, List artists, List albums, final boolean starred, ProgressListener progressListener, Context context) throws Exception {
musicService.setStarred(entries, artists, albums, starred, progressListener, context);
// Fuzzy logic to update parents serialization
List allEntries = new ArrayList();
if(artists != null) {
allEntries.addAll(artists);
}
if(albums != null) {
allEntries.addAll(albums);
}
if (entries != null) {
allEntries.addAll(entries);
}
new StarUpdater(context, allEntries).execute();
}
@Override
public List getShares(Context context, ProgressListener progressListener) throws Exception {
return musicService.getShares(context, progressListener);
}
@Override
public List createShare(List ids, String description, Long expires, Context context, ProgressListener progressListener) throws Exception {
return musicService.createShare(ids, description, expires, context, progressListener);
}
@Override
public void deleteShare(String id, Context context, ProgressListener progressListener) throws Exception {
musicService.deleteShare(id, context, progressListener);
}
@Override
public void updateShare(String id, String description, Long expires, Context context, ProgressListener progressListener) throws Exception {
musicService.updateShare(id, description, expires, context, progressListener);
}
@Override
public List getChatMessages(Long since, Context context, ProgressListener progressListener) throws Exception {
return musicService.getChatMessages(since, context, progressListener);
}
@Override
public void addChatMessage(String message, Context context, ProgressListener progressListener) throws Exception {
musicService.addChatMessage(message, context, progressListener);
}
@Override
public List getGenres(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
List result = null;
if(!refresh) {
result = FileUtil.deserialize(context, getCacheName(context, "genre"), ArrayList.class);
}
if(result == null) {
result = musicService.getGenres(refresh, context, progressListener);
FileUtil.serialize(context, new ArrayList(result), getCacheName(context, "genre"));
}
return result;
}
@Override
public MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context, ProgressListener progressListener) throws Exception {
try {
MusicDirectory dir = musicService.getSongsByGenre(genre, count, offset, context, progressListener);
FileUtil.serialize(context, dir, getCacheName(context, "genreSongs", Integer.toString(offset)));
return dir;
} catch(IOException e) {
MusicDirectory dir = FileUtil.deserialize(context, getCacheName(context, "genreSongs", Integer.toString(offset)), MusicDirectory.class);
if(dir == null) {
// If we are at start and no cache, throw error higher
if(offset == 0) {
throw e;
} else {
// Otherwise just pretend we are at the end of the list
return new MusicDirectory();
}
} else {
return dir;
}
}
}
@Override
public MusicDirectory getTopTrackSongs(String artist, int size, Context context, ProgressListener progressListener) throws Exception {
return musicService.getTopTrackSongs(artist, size, context, progressListener);
}
@Override
public List getPodcastChannels(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
checkSettingsChanged(context);
List result = refresh ? null : cachedPodcastChannels.get();
if (result == null) {
if(!refresh) {
result = FileUtil.deserialize(context, getCacheName(context, "podcast"), ArrayList.class);
}
if(result == null) {
result = musicService.getPodcastChannels(refresh, context, progressListener);
FileUtil.serialize(context, new ArrayList(result), getCacheName(context, "podcast"));
}
cachedPodcastChannels.set(result);
}
return result;
}
@Override
public MusicDirectory getPodcastEpisodes(boolean refresh, String id, Context context, ProgressListener progressListener) throws Exception {
String altId = "p-" + id;
MusicDirectory result = null;
if(!refresh) {
result = FileUtil.deserialize(context, getCacheName(context, "directory", altId), MusicDirectory.class, 10);
}
if(result == null) {
result = musicService.getPodcastEpisodes(refresh, id, context, progressListener);
FileUtil.serialize(context, result, getCacheName(context, "directory", altId));
}
return result;
}
@Override
public MusicDirectory getNewestPodcastEpisodes(int count, Context context, ProgressListener progressListener) throws Exception {
return musicService.getNewestPodcastEpisodes(count, context, progressListener);
}
@Override
public void refreshPodcasts(Context context, ProgressListener progressListener) throws Exception {
musicService.refreshPodcasts(context, progressListener);
}
@Override
public void createPodcastChannel(String url, Context context, ProgressListener progressListener) throws Exception{
musicService.createPodcastChannel(url, context, progressListener);
}
@Override
public void deletePodcastChannel(final String id, Context context, ProgressListener progressListener) throws Exception{
new SerializeUpdater(context, "podcast") {
@Override
public boolean checkResult(PodcastChannel check) {
return id.equals(check.getId());
}
@Override
public void updateResult(List objects, PodcastChannel result) {
objects.remove(result);
cachedPodcastChannels.set(objects);
}
}.execute();
musicService.deletePodcastChannel(id, context, progressListener);
}
@Override
public void downloadPodcastEpisode(String id, Context context, ProgressListener progressListener) throws Exception{
musicService.downloadPodcastEpisode(id, context, progressListener);
}
@Override
public void deletePodcastEpisode(final String id, String parent, ProgressListener progressListener, Context context) throws Exception{
musicService.deletePodcastEpisode(id, parent, progressListener, context);
new MusicDirectoryUpdater(context, "directory", "p-" + parent) {
@Override
public boolean checkResult(Entry check) {
return id.equals(((PodcastEpisode) check).getEpisodeId());
}
@Override
public void updateResult(List objects, Entry result) {
objects.remove(result);
}
}.execute();
}
@Override
public void setRating(final Entry entry, final int rating, Context context, ProgressListener progressListener) throws Exception {
musicService.setRating(entry, rating, context, progressListener);
new GenericEntryUpdater(context, entry) {
@Override
public boolean checkResult(Entry entry, Entry check) {
if (entry.getId().equals(check.getId())) {
check.setRating(entry.getRating());
return true;
}
return false;
}
@Override
public void updateResult(Entry result) {
}
}.execute();
}
@Override
public MusicDirectory getBookmarks(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
MusicDirectory bookmarks = musicService.getBookmarks(refresh, context, progressListener);
MusicDirectory oldBookmarks = FileUtil.deserialize(context, "bookmarks", MusicDirectory.class);
if(oldBookmarks != null) {
final List oldList = oldBookmarks.getChildren();
final List newList = new ArrayList();
newList.addAll(bookmarks.getChildren());
for(Iterator it = oldList.iterator(); it.hasNext(); ) {
Entry oldEntry = it.next();
// Remove entries from newList
int position = newList.indexOf(oldEntry);
if(position != -1) {
Entry newEntry = newList.get(position);
if(newEntry.getBookmark().getPosition() == oldEntry.getBookmark().getPosition()) {
newList.remove(position);
}
// Remove from old regardless of whether position is wrong
it.remove();
} else {
oldEntry.setBookmark(null);
}
}
List totalList = new ArrayList();
totalList.addAll(oldList);
totalList.addAll(newList);
new BookmarkUpdater(context, totalList).execute();
}
FileUtil.serialize(context, bookmarks, "bookmarks");
return bookmarks;
}
@Override
public void createBookmark(Entry entry, int position, String comment, Context context, ProgressListener progressListener) throws Exception {
musicService.createBookmark(entry, position, comment, context, progressListener);
new BookmarkUpdater(context, entry).execute();
}
@Override
public void deleteBookmark(Entry entry, Context context, ProgressListener progressListener) throws Exception {
musicService.deleteBookmark(entry, context, progressListener);
new BookmarkUpdater(context, entry).execute();
}
@Override
public User getUser(boolean refresh, String username, Context context, ProgressListener progressListener) throws Exception {
User result = null;
try {
result = musicService.getUser(refresh, username, context, progressListener);
FileUtil.serialize(context, result, getCacheName(context, "user-" + username));
} catch(Exception e) {
// Don't care
}
if(result == null && !refresh) {
result = FileUtil.deserialize(context, getCacheName(context, "user-" + username), User.class);
}
return result;
}
@Override
public List getUsers(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
List result = null;
if(!refresh) {
result = FileUtil.deserialize(context, getCacheName(context, "users"), ArrayList.class);
}
if(result == null) {
result = musicService.getUsers(refresh, context, progressListener);
FileUtil.serialize(context, new ArrayList(result), getCacheName(context, "users"));
}
return result;
}
@Override
public void createUser(final User user, Context context, ProgressListener progressListener) throws Exception {
musicService.createUser(user, context, progressListener);
new UserUpdater(context, "") {
@Override
public boolean checkResult(User check) {
return true;
}
@Override
public void updateResult(List users, User result) {
users.add(user);
}
}.execute();
}
@Override
public void updateUser(final User user, Context context, ProgressListener progressListener) throws Exception {
musicService.updateUser(user, context, progressListener);
new UserUpdater(context, user.getUsername()) {
@Override
public void updateResult(List users, User result) {
result.setEmail(user.getEmail());
result.setSettings(user.getSettings());
}
}.execute();
}
@Override
public void deleteUser(String username, Context context, ProgressListener progressListener) throws Exception {
musicService.deleteUser(username, context, progressListener);
new UserUpdater(context, username) {
@Override
public void updateResult(List users, User result) {
users.remove(result);
}
}.execute();
}
@Override
public void changeEmail(String username, final String email, Context context, ProgressListener progressListener) throws Exception {
musicService.changeEmail(username, email, context, progressListener);
// Update cached email for user
new UserUpdater(context, username) {
@Override
public void updateResult(List users, User result) {
result.setEmail(email);
}
}.execute();
}
@Override
public void changePassword(String username, String password, Context context, ProgressListener progressListener) throws Exception {
musicService.changePassword(username, password, context, progressListener);
}
@Override
public Bitmap getAvatar(String username, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception {
return musicService.getAvatar(username, size, context, progressListener, task);
}
@Override
public ArtistInfo getArtistInfo(String id, boolean refresh, boolean allowNetwork, Context context, ProgressListener progressListener) throws Exception {
String cacheName = getCacheName(context, "artistInfo", id);
ArtistInfo info = null;
if(!refresh) {
info = FileUtil.deserialize(context, cacheName, ArtistInfo.class);
}
if(info == null && allowNetwork) {
info = musicService.getArtistInfo(id, refresh, allowNetwork, context, progressListener);
FileUtil.serialize(context, info, cacheName);
}
return info;
}
@Override
public Bitmap getBitmap(String url, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception {
return musicService.getBitmap(url, size, context, progressListener, task);
}
@Override
public MusicDirectory getVideos(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
try {
MusicDirectory dir = musicService.getVideos(refresh, context, progressListener);
FileUtil.serialize(context, dir, "videos");
return dir;
} catch(IOException e) {
MusicDirectory dir = FileUtil.deserialize(context, "videos", MusicDirectory.class);
if(dir == null) {
throw e;
} else {
return dir;
}
}
}
@Override
public void savePlayQueue(List songs, Entry currentPlaying, int position, Context context, ProgressListener progressListener) throws Exception {
musicService.savePlayQueue(songs, currentPlaying, position, context, progressListener);
}
@Override
public PlayerQueue getPlayQueue(Context context, ProgressListener progressListener) throws Exception {
return musicService.getPlayQueue(context, progressListener);
}
@Override
public int processOfflineSyncs(final Context context, final ProgressListener progressListener) throws Exception{
return musicService.processOfflineSyncs(context, progressListener);
}
@Override
public void setInstance(Integer instance) throws Exception {
musicService.setInstance(instance);
}
private String getCacheName(Context context, String name, String id) {
String s = musicService.getRestUrl(context, null, false) + id;
return name + "-" + s.hashCode() + ".ser";
}
private String getCacheName(Context context, String name) {
String s = musicService.getRestUrl(context, null, false);
return name + "-" + s.hashCode() + ".ser";
}
private void deleteRemovedEntries(Context context, MusicDirectory dir, MusicDirectory cached) {
if(cached != null) {
List oldList = new ArrayList();
oldList.addAll(cached.getChildren());
// Remove all current items from old list
for(Entry entry: dir.getChildren()) {
oldList.remove(entry);
}
// Anything remaining has been removed from server
MediaStoreService store = new MediaStoreService(context);
for(Entry entry: oldList) {
File file = FileUtil.getEntryFile(context, entry);
FileUtil.recursiveDelete(file, store);
}
}
}
private abstract class SerializeUpdater {
final Context context;
final String cacheName;
final boolean singleUpdate;
public SerializeUpdater(Context context, String cacheName) {
this(context, cacheName, true);
}
public SerializeUpdater(Context context, String cacheName, boolean singleUpdate) {
this.context = context;
this.cacheName = getCacheName(context, cacheName);
this.singleUpdate = singleUpdate;
}
public SerializeUpdater(Context context, String cacheName, String id) {
this(context, cacheName, id, true);
}
public SerializeUpdater(Context context, String cacheName, String id, boolean singleUpdate) {
this.context = context;
this.cacheName = getCacheName(context, cacheName, id);
this.singleUpdate = singleUpdate;
}
public ArrayList getArrayList() {
return FileUtil.deserialize(context, cacheName, ArrayList.class);
}
public abstract boolean checkResult(T check);
public abstract void updateResult(List objects, T result);
public void save(ArrayList objects) {
FileUtil.serialize(context, objects, cacheName);
}
public void execute() {
ArrayList objects = getArrayList();
// Only execute if something to check against
if(objects != null) {
List results = new ArrayList();
for(T check: objects) {
if(checkResult(check)) {
results.add(check);
if(singleUpdate) {
break;
}
}
}
// Iterate through and update each object matched
for(T result: results) {
updateResult(objects, result);
}
// Only reserialize if at least one match was found
if(results.size() > 0) {
save(objects);
}
}
}
}
private abstract class UserUpdater extends SerializeUpdater {
String username;
public UserUpdater(Context context, String username) {
super(context, "users");
this.username = username;
}
@Override
public boolean checkResult(User check) {
return username.equals(check.getUsername());
}
}
private abstract class PlaylistUpdater extends SerializeUpdater {
String id;
public PlaylistUpdater(Context context, String id) {
super(context, "playlist");
this.id = id;
}
@Override
public boolean checkResult(Playlist check) {
return id.equals(check.getId());
}
}
private abstract class MusicDirectoryUpdater extends SerializeUpdater {
protected MusicDirectory musicDirectory;
public MusicDirectoryUpdater(Context context, String cacheName, String id) {
super(context, cacheName, id, true);
}
public MusicDirectoryUpdater(Context context, String cacheName, String id, boolean singleUpdate) {
super(context, cacheName, id, singleUpdate);
}
@Override
public ArrayList getArrayList() {
musicDirectory = FileUtil.deserialize(context, cacheName, MusicDirectory.class);
if(musicDirectory != null) {
return new ArrayList<>(musicDirectory.getChildren());
} else {
return null;
}
}
public void save(ArrayList objects) {
musicDirectory.replaceChildren(objects);
FileUtil.serialize(context, musicDirectory, cacheName);
}
}
private abstract class PlaylistDirectoryUpdater {
Context context;
public PlaylistDirectoryUpdater(Context context) {
this.context = context;
}
public abstract boolean checkResult(Entry check);
public abstract void updateResult(Entry result);
public void execute() {
List playlists = FileUtil.deserialize(context, getCacheName(context, "playlist"), ArrayList.class);
if(playlists == null) {
// No playlist list cache, nothing to update!
return;
}
for(Playlist playlist: playlists) {
new MusicDirectoryUpdater(context, "playlist", playlist.getId(), false) {
@Override
public boolean checkResult(Entry check) {
return PlaylistDirectoryUpdater.this.checkResult(check);
}
@Override
public void updateResult(List objects, Entry result) {
PlaylistDirectoryUpdater.this.updateResult(result);
}
}.execute();
}
}
}
private abstract class GenericEntryUpdater {
Context context;
List entries;
public GenericEntryUpdater(Context context, Entry entry) {
this.context = context;
this.entries = Arrays.asList(entry);
}
public GenericEntryUpdater(Context context, List entries) {
this.context = context;
this.entries = entries;
}
public boolean checkResult(Entry entry, Entry check) {
return entry.getId().equals(check.getId());
}
public abstract void updateResult(Entry result);
public void execute() {
String cacheName, parent;
// Make sure it is up to date
isTagBrowsing = Util.isTagBrowsing(context, musicService.getInstance(context));
// Run through each entry, trying to update the directory it is in
final List songs = new ArrayList();
for(final Entry entry: entries) {
if(isTagBrowsing) {
// If starring album, needs to reference artist instead
if(entry.isDirectory()) {
if(entry.isAlbum()) {
cacheName = "artist";
parent = entry.getArtistId();
} else {
cacheName = "artists";
parent = null;
}
} else {
cacheName = "album";
parent = entry.getAlbumId();
}
} else {
if(entry.isDirectory() && !entry.isAlbum()) {
cacheName = "indexes";
parent = null;
} else {
cacheName = "directory";
parent = entry.getParent();
}
}
// Parent is only null when it is an artist
if(parent == null) {
new IndexesUpdater(context, cacheName) {
@Override
public boolean checkResult(Artist check) {
return GenericEntryUpdater.this.checkResult(entry, new Entry(check));
}
@Override
public void updateResult(List objects, Artist result) {
// Don't try to put anything here, as the Entry update method will not be called since it's a artist!
}
}.execute();
} else {
new MusicDirectoryUpdater(context, cacheName, parent) {
@Override
public boolean checkResult(Entry check) {
return GenericEntryUpdater.this.checkResult(entry, check);
}
@Override
public void updateResult(List objects, Entry result) {
GenericEntryUpdater.this.updateResult(result);
}
}.execute();
}
if(entry instanceof PodcastEpisode) {
new MusicDirectoryUpdater(context, cacheName, "p-" + entry.getParent()) {
@Override
public boolean checkResult(Entry check) {
return GenericEntryUpdater.this.checkResult(entry, check);
}
@Override
public void updateResult(List objects, Entry result) {
GenericEntryUpdater.this.updateResult(result);
}
}.execute();
} else if(!entry.isDirectory()) {
songs.add(entry);
}
}
// Only run through playlists once and check each song against it
if(songs.size() > 0) {
new PlaylistDirectoryUpdater(context) {
@Override
public boolean checkResult(Entry check) {
for(Entry entry: songs) {
if(GenericEntryUpdater.this.checkResult(entry, check)) {
return true;
}
}
return false;
}
@Override
public void updateResult(Entry result) {
GenericEntryUpdater.this.updateResult(result);
}
}.execute();
}
}
}
private class BookmarkUpdater extends GenericEntryUpdater {
public BookmarkUpdater(Context context, Entry entry) {
super(context, entry);
}
public BookmarkUpdater(Context context, List entries) {
super(context, entries);
}
@Override
public boolean checkResult(Entry entry, Entry check) {
if(entry.getId().equals(check.getId())) {
int position;
if(entry.getBookmark() == null) {
position = -1;
} else {
position = entry.getBookmark().getPosition();
}
if(position == -1 && check.getBookmark() != null) {
check.setBookmark(null);
return true;
} else if(position >= 0 && (check.getBookmark() == null || check.getBookmark().getPosition() != position)) {
Bookmark bookmark = check.getBookmark();
// Create one if empty
if(bookmark == null) {
bookmark = new Bookmark();
check.setBookmark(bookmark);
}
// Update bookmark position no matter what
bookmark.setPosition(position);
return true;
}
}
return false;
}
@Override
public void updateResult(Entry result) {
}
}
private class StarUpdater extends GenericEntryUpdater {
public StarUpdater(Context context, List entries) {
super(context, entries);
}
@Override
public boolean checkResult(Entry entry, Entry check) {
if (entry.getId().equals(check.getId())) {
if(entry.isStarred() != check.isStarred()) {
check.setStarred(entry.isStarred());
return true;
}
}
return false;
}
@Override
public void updateResult(Entry result) {
}
};
private abstract class IndexesUpdater extends SerializeUpdater {
Indexes indexes;
IndexesUpdater(Context context, String name) {
super(context, name, Util.getSelectedMusicFolderId(context, musicService.getInstance(context)));
}
@Override
public ArrayList getArrayList() {
indexes = FileUtil.deserialize(context, cacheName, Indexes.class);
if(indexes == null) {
return null;
}
ArrayList artists = new ArrayList();
artists.addAll(indexes.getArtists());
artists.addAll(indexes.getShortcuts());
return artists;
}
public void save(ArrayList objects) {
indexes.setArtists(objects);
FileUtil.serialize(context, indexes, cacheName);
cachedIndexes.set(indexes);
}
}
protected void updateAllSongs(Context context, MusicDirectory dir) {
List songs = dir.getSongs();
if(!songs.isEmpty()) {
SongDBHandler.getHandler(context).addSongs(musicService.getInstance(context), songs);
}
}
private void checkSettingsChanged(Context context) {
int instance = musicService.getInstance(context);
String newUrl = musicService.getRestUrl(context, null, false);
boolean newIsTagBrowsing = Util.isTagBrowsing(context, instance);
if (!Util.equals(newUrl, restUrl) || isTagBrowsing != newIsTagBrowsing) {
cachedMusicFolders.clear();
cachedLicenseValid.clear();
cachedIndexes.clear();
cachedPlaylists.clear();
cachedPodcastChannels.clear();
restUrl = newUrl;
isTagBrowsing = newIsTagBrowsing;
}
String newMusicFolderId = Util.getSelectedMusicFolderId(context, instance);
if(!Util.equals(newMusicFolderId, musicFolderId)) {
cachedIndexes.clear();
musicFolderId = newMusicFolderId;
}
}
public RESTMusicService getMusicService() {
return musicService;
}
}