/* == This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
*
* Copyright 2016, Enno Gottschalk <mrmaffen@googlemail.com>
*
* Tomahawk 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.
*
* Tomahawk 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 Tomahawk. If not, see <http://www.gnu.org/licenses/>.
*/
package org.tomahawk.libtomahawk.collection;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.jdeferred.AlwaysCallback;
import org.jdeferred.Deferred;
import org.jdeferred.DoneCallback;
import org.jdeferred.FailCallback;
import org.jdeferred.Promise;
import org.tomahawk.libtomahawk.infosystem.stations.ScriptPlaylistGenerator;
import org.tomahawk.libtomahawk.infosystem.stations.ScriptPlaylistGeneratorManager;
import org.tomahawk.libtomahawk.infosystem.stations.ScriptPlaylistGeneratorResult;
import org.tomahawk.libtomahawk.infosystem.stations.ScriptPlaylistGeneratorSearchResult;
import org.tomahawk.libtomahawk.resolver.Query;
import org.tomahawk.libtomahawk.utils.ADeferredObject;
import org.tomahawk.libtomahawk.utils.GsonHelper;
import android.support.v4.util.Pair;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class StationPlaylist extends Playlist {
private String mSessionId;
private Playlist mPlaylist;
private List<Pair<Artist, String>> mArtists;
private List<Pair<Track, String>> mTracks;
private List<String> mGenres;
private final List<Query> mCandidates = new ArrayList<>();
private long mCreatedTimeStamp = 0L;
private long mPlayedTimeStamp = 0L;
private Deferred<List<Query>, Throwable, Void> mFillDeferred;
private StationPlaylist(List<Pair<Artist, String>> artists, List<Pair<Track, String>> tracks,
List<String> genres) {
super(getCacheKey(artists, tracks, genres));
mArtists = artists;
mTracks = tracks;
mGenres = genres;
String name = "";
if (mArtists != null) {
for (Pair<Artist, String> artist : mArtists) {
if (!name.isEmpty()) {
name += ", ";
}
name += artist.first.getPrettyName();
}
}
if (mTracks != null) {
for (Pair<Track, String> track : mTracks) {
if (!name.isEmpty()) {
name += ", ";
}
name += track.first.getArtist().getPrettyName() + " - " + track.first.getName();
}
}
if (mGenres != null) {
for (String genre : mGenres) {
if (!name.isEmpty()) {
name += ", ";
}
name += genre;
}
}
setName(name);
mCreatedTimeStamp = System.currentTimeMillis();
}
private StationPlaylist(Playlist playlist) {
super(getCacheKey(playlist));
mPlaylist = playlist;
setName(mPlaylist.getName());
mCreatedTimeStamp = System.currentTimeMillis();
}
private static String getCacheKey(Playlist playlist) {
return "station_" + playlist.getCacheKey();
}
private static String getCacheKey(List<Pair<Artist, String>> artists,
List<Pair<Track, String>> tracks, List<String> genres) {
String key = "station_";
if (artists != null) {
Collections.sort(artists, new Comparator<Pair<Artist, String>>() {
@Override
public int compare(Pair<Artist, String> lhs, Pair<Artist, String> rhs) {
return lhs.first.getName().compareToIgnoreCase(rhs.first.getName());
}
});
for (Pair<Artist, String> artist : artists) {
key += "♠" + artist.first.getCacheKey();
}
}
if (tracks != null) {
Collections.sort(tracks, new Comparator<Pair<Track, String>>() {
@Override
public int compare(Pair<Track, String> lhs, Pair<Track, String> rhs) {
return lhs.first.getName().compareToIgnoreCase(rhs.first.getName());
}
});
for (Pair<Track, String> track : tracks) {
key += "♠" + track.first.getCacheKey();
}
}
if (genres != null) {
Collections.sort(genres);
for (String genre : genres) {
key += "♠" + genre;
}
}
return key;
}
public static StationPlaylist get(String json) {
JsonObject jsonObject = GsonHelper.get().fromJson(json, JsonObject.class);
List<Pair<Artist, String>> artists = null;
if (jsonObject.has("artists") && jsonObject.get("artists").isJsonArray()) {
artists = new ArrayList<>();
for (JsonElement element : jsonObject.getAsJsonArray("artists")) {
if (element.isJsonObject()) {
JsonObject o = element.getAsJsonObject();
Artist artist = Artist.get(o.get("artist").getAsString());
JsonElement idElement = o.get(getIdKey());
String id = "";
if (idElement != null) {
id = idElement.getAsString();
}
artists.add(new Pair<>(artist, id));
}
}
}
List<Pair<Track, String>> tracks = null;
if (jsonObject.has("tracks") && jsonObject.get("tracks").isJsonArray()) {
tracks = new ArrayList<>();
for (JsonElement element : jsonObject.getAsJsonArray("tracks")) {
if (element.isJsonObject()) {
JsonObject o = element.getAsJsonObject();
Artist artist = Artist.get(o.get("artist").getAsString());
Album album = Album.get(o.get("album").getAsString(), artist);
Track track = Track.get(o.get("track").getAsString(), album, artist);
JsonElement idElement = o.get(getIdKey());
String id = "";
if (idElement != null) {
id = idElement.getAsString();
}
tracks.add(new Pair<>(track, id));
}
}
}
List<String> genres = null;
if (jsonObject.has("genres") && jsonObject.get("genres").isJsonArray()) {
genres = new ArrayList<>();
for (JsonElement element : jsonObject.getAsJsonArray("genres")) {
if (element.isJsonObject()) {
JsonObject o = element.getAsJsonObject();
genres.add(o.get("name").getAsString());
}
}
}
return get(artists, tracks, genres);
}
public static StationPlaylist get(List<Pair<Artist, String>> artists,
List<Pair<Track, String>> tracks, List<String> genres) {
Cacheable cacheable = get(Playlist.class, getCacheKey(artists, tracks, genres));
return cacheable != null ? (StationPlaylist) cacheable
: new StationPlaylist(artists, tracks, genres);
}
public static StationPlaylist get(Playlist playlist) {
Cacheable cacheable = get(Playlist.class, getCacheKey(playlist));
return cacheable != null ? (StationPlaylist) cacheable : new StationPlaylist(playlist);
}
public List<Pair<Artist, String>> getArtists() {
return mArtists;
}
public List<Pair<Track, String>> getTracks() {
return mTracks;
}
public List<String> getGenres() {
return mGenres;
}
public Playlist getPlaylist() {
return mPlaylist;
}
public void setCreatedTimeStamp(long createdTimeStamp) {
mCreatedTimeStamp = createdTimeStamp;
}
public long getCreatedTimeStamp() {
return mCreatedTimeStamp;
}
public long getPlayedTimeStamp() {
return mPlayedTimeStamp;
}
public void setPlayedTimeStamp(long playedTimeStamp) {
mPlayedTimeStamp = playedTimeStamp;
}
public Promise<List<Query>, Throwable, Void> fillPlaylist(final int limit) {
if (mFillDeferred != null && mFillDeferred.isPending()) {
return null;
}
final Deferred<List<Query>, Throwable, Void> fillDeferred = new ADeferredObject<>();
mFillDeferred = fillDeferred;
pickSeedsFromPlaylist().done(new DoneCallback<Void>() {
@Override
public void onDone(Void result) {
ScriptPlaylistGenerator generator =
ScriptPlaylistGeneratorManager.get().getDefaultPlaylistGenerator();
if (mCandidates.size() >= limit) {
// We got enough candidates in cache
List<Query> queries = new ArrayList<>();
for (int i = 0; i < limit; i++) {
queries.add(mCandidates.remove(0));
}
fillDeferred.resolve(queries);
} else if (generator != null) {
generator.fillPlaylist(mSessionId, mArtists, mTracks, mGenres)
.done(new DoneCallback<ScriptPlaylistGeneratorResult>() {
@Override
public void onDone(ScriptPlaylistGeneratorResult result) {
mSessionId = result.sessionId;
List<Query> queries = new ArrayList<>();
if (result.results != null) {
int actualLimit = Math.min(result.results.size(), limit);
for (int i = 0; i < actualLimit; i++) {
queries.add(result.results.remove(0));
}
// Add the rest to our candidate cache
mCandidates.addAll(result.results);
}
if (queries.size() == 0) {
fillDeferred.reject(
new Throwable("Couldn't find suitable tracks"));
} else {
fillDeferred.resolve(queries);
}
}
})
.fail(new FailCallback<Throwable>() {
@Override
public void onFail(Throwable result) {
fillDeferred.reject(result);
}
});
}
}
});
return mFillDeferred;
}
public String toJson() {
JsonObject json = new JsonObject();
if (mArtists != null) {
JsonArray artists = new JsonArray();
for (Pair<Artist, String> artist : mArtists) {
JsonObject o = new JsonObject();
o.addProperty("artist", artist.first.getName());
o.addProperty(getIdKey(), artist.second);
artists.add(o);
}
json.add("artists", artists);
}
if (mTracks != null) {
JsonArray tracks = new JsonArray();
for (Pair<Track, String> track : mTracks) {
JsonObject o = new JsonObject();
o.addProperty("track", track.first.getName());
o.addProperty("artist", track.first.getArtist().getName());
o.addProperty("album", track.first.getAlbum().getName());
o.addProperty(getIdKey(), track.second);
tracks.add(o);
}
json.add("tracks", tracks);
}
if (mGenres != null) {
JsonArray genres = new JsonArray();
for (String genre : mGenres) {
JsonObject o = new JsonObject();
o.addProperty("name", genre);
genres.add(o);
}
json.add("genres", genres);
}
return GsonHelper.get().toJson(json);
}
private Promise<Void, Void, Throwable> pickSeedsFromPlaylist() {
ADeferredObject<Void, Void, Throwable> deferred = new ADeferredObject<>();
if (mPlaylist == null || mTracks != null) {
// No need to do anything
deferred.resolve(null);
} else if (mPlaylist.size() < 5) {
// No need to pick random tracks, we use all of them anyways
List<Pair<Track, String>> tracks = new ArrayList<>();
for (PlaylistEntry entry : mPlaylist.getEntries()) {
// Let the js resolver fetch the track ids for us
tracks.add(new Pair<>(entry.getQuery().getBasicTrack(), ""));
}
mTracks = tracks;
deferred.resolve(null);
} else {
pickSeedsFromPlaylist(deferred, new ArrayList<Integer>(),
new ArrayList<Pair<Track, String>>(), 10);
}
return deferred;
}
private void pickSeedsFromPlaylist(final ADeferredObject<Void, Void, Throwable> deferred,
final List<Integer> pickedIndexes, final List<Pair<Track, String>> tracks,
int attemptCount) {
if (attemptCount-- < 0 || tracks.size() >= 5) {
mTracks = tracks;
deferred.resolve(null);
return;
}
int size = mPlaylist.size();
int randomIndex = (int) (Math.random() * size);
while (pickedIndexes.contains(randomIndex)) {
if (randomIndex + 1 < size) {
randomIndex++;
} else {
randomIndex = 0;
}
}
pickedIndexes.add(randomIndex);
PlaylistEntry entry = mPlaylist.getEntryAtPos(randomIndex);
Track candidate = entry.getQuery().getBasicTrack();
final int finalAttemptCount = attemptCount;
ScriptPlaylistGenerator generator =
ScriptPlaylistGeneratorManager.get().getDefaultPlaylistGenerator();
if (generator != null) {
generator.search("track:" + candidate.getName()
+ "%20artist:" + candidate.getArtist().getName()).always(
new AlwaysCallback<ScriptPlaylistGeneratorSearchResult, Throwable>() {
@Override
public void onAlways(Promise.State state,
ScriptPlaylistGeneratorSearchResult resolved, Throwable rejected) {
if (resolved != null && resolved.mTracks != null
&& resolved.mTracks.size() > 0) {
tracks.add(resolved.mTracks.get(0));
}
pickSeedsFromPlaylist(deferred, pickedIndexes, tracks,
finalAttemptCount);
}
});
}
}
private static String getIdKey() {
return ScriptPlaylistGeneratorManager.get().getDefaultPlaylistGeneratorId() + "_id";
}
@Override
public String toString() {
return getClass().getSimpleName() + "( id: " + getId() + ", name: " + getName()
+ ", size: " + size() + " )@" + Integer.toHexString(hashCode());
}
}