/*
* Copyright (C) 2014 Fastboot Mobile, LLC.
*
* This program 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.
*
* This program 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 this program;
* if not, see <http://www.gnu.org/licenses>.
*/
package com.fastbootmobile.encore.api.echonest;
import android.util.Log;
import com.echonest.api.v4.DynamicPlaylistSession;
import com.echonest.api.v4.DynamicPlaylistSteerParams;
import com.echonest.api.v4.EchoNestException;
import com.echonest.api.v4.GeneralCatalog;
import com.echonest.api.v4.Playlist;
import com.echonest.api.v4.Song;
import com.echonest.api.v4.Track;
import com.fastbootmobile.encore.framework.PlaybackProxy;
import com.fastbootmobile.encore.providers.ProviderAggregator;
import java.util.List;
/**
* Represents a parametrized AutoMix Bucket
*/
public class AutoMixBucket {
private static final String TAG = "AutoMixBucket";
private static final boolean DEBUG = true;
String mName;
String[] mStyles;
String[] mMoods;
boolean mUseTaste;
float mAdventurousness;
String[] mSongTypes;
float mSpeechiness;
float mEnergy;
float mFamiliar;
private DynamicPlaylistSession mPlaylistSession;
private GeneralCatalog mCatalog;
private boolean mSessionReady;
private boolean mSessionError;
/**
* Creates a new Automix Bucket without an existing attached session. You should not create
* manually an automix bucket, but rather use {@link com.fastbootmobile.encore.api.echonest.AutoMixManager}
* @param name The name of the bucket
* @param styles The EchoNest styles to include in the bucket
* @param moods The EchoNest moods to include
* @param taste Whether or not this bucket uses the user's taste profile. Note that you must
* release the catalog once done using releaseCatalog
* @param adventurous The level of adventurousness [0.0-1.0]
* @param songTypes The EchoNest types of songs to include
* @param speechiness The target level of speechiness [0.0-1.0]
* @param energy The target level of energy [0.0-1.0]
* @param familiar The target level of familiarity [0.0-1.0]
*/
AutoMixBucket(String name, String[] styles, String[] moods, boolean taste, float adventurous,
String[] songTypes, float speechiness, float energy, float familiar) {
this(name, styles, moods, taste, adventurous, songTypes, speechiness, energy, familiar, null);
mSessionReady = false;
mSessionError = false;
}
/**
* Creates a new Automix Bucket with an existing session ID. You should not create manually an
* automix bucket, but rather use {@link com.fastbootmobile.encore.api.echonest.AutoMixManager}
* @param name The name of the bucket
* @param styles The EchoNest styles to include in the bucket
* @param moods The EchoNest moods to include
* @param taste Whether or not this bucket uses the user's taste profile. Note that you must
* release the catalog once done using releaseCatalog
* @param adventurous The level of adventurousness [0.0-1.0]
* @param songTypes The EchoNest types of songs to include
* @param speechiness The target level of speechiness [0.0-1.0]
* @param energy The target level of energy [0.0-1.0]
* @param familiar The target level of familiarity [0.0-1.0]
* @param sessionId The existing EchoNest session ID to recover
*/
AutoMixBucket(String name, String[] styles, String[] moods, boolean taste, float adventurous,
String[] songTypes, float speechiness, float energy, float familiar,
String sessionId) {
mName = name;
mStyles = styles;
mMoods = moods;
mUseTaste = taste;
mAdventurousness = adventurous;
mSongTypes = songTypes;
mSpeechiness = speechiness;
mEnergy = energy;
mFamiliar = familiar;
if (sessionId != null) {
mPlaylistSession = new DynamicPlaylistSession(new EchoNest().getApi(), sessionId);
mSessionReady = true;
} else {
mSessionReady = false;
}
mSessionError = false;
}
/**
* Creates or reset the EchoNest session for this bucket. This will reset the bucket on the
* EchoNest end and initialize a new session.
* @return The generated session
*/
DynamicPlaylistSession createPlaylistSession() {
// Reset session status as we could be retrying
mSessionReady = false;
mSessionError = false;
EchoNest echoNest = new EchoNest();
String type = generateTasteCatalogIfNeededAndGetType(echoNest);
// Generate the playlist session with the initial parameters
if (!mSessionError) {
if (DEBUG) Log.d(TAG, "Creating dynamic playlist.");
String catalogId = (mCatalog != null ? mCatalog.getID() : null);
try {
mPlaylistSession = echoNest.createDynamicPlaylist(type, catalogId, mMoods, mStyles);
} catch (EchoNestException e) {
Log.e(TAG, "Unable to create the dynamic playlist", e);
mSessionError = true;
}
}
// If all went well, steer the dynamic playlist with the advanced settings
if (!mSessionError) {
DynamicPlaylistSteerParams p = new DynamicPlaylistSteerParams();
if (mEnergy >= 0) {
p.addTargetValue(DynamicPlaylistSteerParams.SteeringParameter.energy, mEnergy);
}
if (mFamiliar >= 0) {
p.addTargetValue(DynamicPlaylistSteerParams.SteeringParameter.artist_familiarity,
mFamiliar);
}
if (mSpeechiness >= 0) {
p.add("target_speechiness", mSpeechiness);
}
if (mAdventurousness >= 0) {
p.setAdventurousness(mAdventurousness);
}
if (mSongTypes != null && mSongTypes.length > 0) {
for (String songType : mSongTypes) {
if (!songType.isEmpty()) {
p.add("song_type", songType);
}
}
}
try {
mPlaylistSession.steer(p);
} catch (EchoNestException e) {
Log.e(TAG, "Unable to steer session data", e);
mSessionError = true;
}
}
mSessionReady = true;
return mPlaylistSession;
}
/**
* Generates a taste profile catalog if needed, and return the type of
* @param echoNest The echonest api connection
* @return 'catalog-radio' or 'artist-description'
*/
private String generateTasteCatalogIfNeededAndGetType(EchoNest echoNest) {
String type;
if (mUseTaste) {
// Generate the taste profile first for use in the playlist session
if (DEBUG) Log.d(TAG, "Generating taste profile...");
try {
mCatalog = echoNest.createTemporaryTasteProfile();
} catch (EchoNestException e) {
Log.e(TAG, "Unable to create the taste profile", e);
mSessionError = true;
} finally {
if ((mMoods == null || mMoods.length == 0)
&& (mStyles == null || mStyles.length == 0)) {
type = "catalog-radio";
} else {
type = "artist-description";
}
if (DEBUG) Log.d(TAG, "Taste profile generation succeeded.");
}
} else {
type = "artist-description";
}
return type;
}
/**
* When the taste profile is enabled, you MUST release (delete) the catalog, as we have
* a limit of 1000 catalogs for a non-commercial account (hence why we only allow that
* for static playlists, so that we can generate that on the fly).
*/
private void releaseCatalog() {
if (mCatalog != null) {
try {
mCatalog.delete();
} catch (EchoNestException e) {
Log.e(TAG, "Unable to delete catalog", e);
}
}
}
public List<String> generateStaticPlaylist() {
EchoNest echoNest = new EchoNest();
String type = generateTasteCatalogIfNeededAndGetType(echoNest);
String seedCatalog = null;
if (mCatalog != null) {
seedCatalog = mCatalog.getID();
}
try {
List<String> songs = echoNest.createStaticPlaylist(type, seedCatalog, mStyles, mMoods, mAdventurousness,
mSongTypes, mSpeechiness, mEnergy, mFamiliar);
releaseCatalog();
return songs;
} catch (EchoNestException e) {
Log.e(TAG, "Cannot create playlist", e);
mSessionError = true;
}
// If we reach here, we got an error
return null;
}
/**
* Returns whether or not the playlist session is ready to be played
* @return True if the playlist session is ready to be played, false otherwise
*/
public boolean isPlaylistSessionReady() {
return mSessionReady;
}
/**
* Returns whether or not the playlist session couldn't be generated
* @return True if an error occured during {#createPlaylistSession}
*/
public boolean isPlaylistSessionError() {
return mSessionError;
}
/**
* Returns the ID of the current playlist session. The session must be ready for this call
* to be valid.
* @return The ID of the current session, or null if none.
*/
public String getSessionId() {
if (mPlaylistSession != null) {
return mPlaylistSession.getSessionID();
} else {
return null;
}
}
/**
* Advance and fetches a new track from this automix bucket.
* @return A Rosetta-stone ID of the next track, or null if no provider supports rosetta-stone
* @throws EchoNestException In case of remote error.
*/
public String getNextTrack() throws EchoNestException {
if (mPlaylistSession == null) {
Log.w(TAG, "getNextTrack called without a playlist session. Creating it");
createPlaylistSession();
}
if (isPlaylistSessionError()) {
Log.e(TAG, "Cannot get next track because playlist session got in error");
throw new EchoNestException(2, "Internal error");
}
final String prefix = ProviderAggregator.getDefault().getPreferredRosettaStonePrefix();
if (prefix != null) {
Playlist nextTracks = null;
int tries = 0;
while (nextTracks == null) {
try {
nextTracks = mPlaylistSession.next();
} catch (EchoNestException e) {
if ((e.getCode() == -1 && e.getMessage().contains("timed out") || e.getMessage().contains("Parse Exception"))
&& tries < 5) {
tries++;
} else {
throw e;
}
} catch (Exception e) {
if (tries < 5) {
Log.e(TAG, "Error while parsing response from EchoNest");
tries++;
} else {
return null;
}
}
}
final List<Song> songs = nextTracks.getSongs();
if (songs.size() > 0) {
final Song firstSong = songs.get(0);
final Track track = firstSong.getTrack(prefix);
if (track != null) {
return track.getForeignID();
} else {
Log.e(TAG, "Null track!");
return null;
}
} else {
Log.e(TAG, "No new track for this bucket!");
}
}
return null;
}
/**
* Returns the name of this bucket
* @return The name of this bucket
*/
public String getName() {
return mName;
}
/**
* Notifies the system that the user liked this song and want more
*/
public void notifyLike() throws EchoNestException {
com.fastbootmobile.encore.model.Song currentTrack = PlaybackProxy.getCurrentTrack();
if (currentTrack != null) {
String songRef = currentTrack.getRef();
try {
mPlaylistSession.feedback(DynamicPlaylistSession.FeedbackType.favorite_song, songRef);
} catch (Exception e) {
Log.e(TAG, "Cannot feedback like", e);
}
}
}
/**
* Notifies the system that the user disliked this song and don't want it again
* @throws EchoNestException
*/
public void notifyDislike() throws EchoNestException {
com.fastbootmobile.encore.model.Song currentTrack = PlaybackProxy.getCurrentTrack();
if (currentTrack != null) {
String songRef = currentTrack.getRef();
try {
mPlaylistSession.feedback(DynamicPlaylistSession.FeedbackType.ban_song, songRef);
} catch (Exception e) {
Log.e(TAG, "Cannot feedback dislike", e);
}
}
}
/**
* Notifies the system that the user skipped the current song so he might not like it
*/
public void notifySkip() throws EchoNestException {
com.fastbootmobile.encore.model.Song currentTrack = PlaybackProxy.getCurrentTrack();
if (currentTrack != null) {
String songRef = currentTrack.getRef();
try {
mPlaylistSession.feedback(DynamicPlaylistSession.FeedbackType.skip_song, songRef);
} catch (Exception e) {
Log.e(TAG, "Cannot feedback skip", e);
}
}
}
}