/*
* Copyright 2008-2013, ETH Zürich, Samuel Welten, Michael Kuhn, Tobias Langner,
* Sandro Affentranger, Lukas Bossard, Michael Grob, Rahul Jain,
* Dominic Langenegger, Sonia Mayor Alonso, Roger Odermatt, Tobias Schlueter,
* Yannick Stucki, Sebastian Wendland, Samuel Zehnder, Samuel Zihlmann,
* Samuel Zweifel
*
* This file is part of Jukefox.
*
* Jukefox 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 any later version. Jukefox 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
* Jukefox. If not, see <http://www.gnu.org/licenses/>.
*/
package ch.ethz.dcg.jukefox.playmode;
import java.util.Collections;
import java.util.List;
import ch.ethz.dcg.jukefox.commons.Constants;
import ch.ethz.dcg.jukefox.commons.DataUnavailableException;
import ch.ethz.dcg.jukefox.commons.utils.Log;
import ch.ethz.dcg.jukefox.commons.utils.Utils;
import ch.ethz.dcg.jukefox.model.AbstractCollectionModelManager;
import ch.ethz.dcg.jukefox.model.AbstractPlayerModelManager;
import ch.ethz.dcg.jukefox.model.collection.BaseAlbum;
import ch.ethz.dcg.jukefox.model.collection.BaseArtist;
import ch.ethz.dcg.jukefox.model.collection.BaseSong;
import ch.ethz.dcg.jukefox.model.collection.IReadOnlyPlaylist;
import ch.ethz.dcg.jukefox.model.collection.PlaylistSong;
import ch.ethz.dcg.jukefox.model.collection.PlaylistSong.SongSource;
import ch.ethz.dcg.jukefox.model.commons.NoNextSongException;
import ch.ethz.dcg.jukefox.model.commons.PlaylistPositionOutOfRangeException;
import ch.ethz.dcg.jukefox.model.player.PlayModeType;
import ch.ethz.dcg.jukefox.model.player.PlayerAction;
/**
* Plays similar songs to the first song being played.
*
* TODO: this mode is broken.
*/
public class SimilarPlayMode extends BasePlayMode {
public static final String TAG = SimilarPlayMode.class.getSimpleName();
private static final int NUM_CANDIDATES = 5;
// private final LinkedHashSet<Integer> recentSongHistory = new
// LinkedHashSet<Integer>();
// private final LinkedHashSet<Integer> recentArtistHistory = new
// LinkedHashSet<Integer>();
private BaseSong<BaseArtist, BaseAlbum> seedSong;
// private boolean isInitialized = false;
private int similarArtistAvoidanceNumber;
private int equalSongAvoidanceNumber;
public SimilarPlayMode(AbstractCollectionModelManager collectionModel, AbstractPlayerModelManager playerModel,
int artistAvoidance, int equalSongAvoidance, BaseSong<BaseArtist, BaseAlbum> seedSong) {
super(collectionModel, playerModel);
this.seedSong = seedSong;
similarArtistAvoidanceNumber = artistAvoidance;
equalSongAvoidanceNumber = equalSongAvoidance;
}
@Override
public PlayerControllerCommands initialize(IReadOnlyPlaylist currentPlaylist) {
PlayerControllerCommands changes = deleteSubsequentSongs(currentPlaylist);
// isInitialized = true;
return changes;
}
private PlayerControllerCommands deleteSubsequentSongs(IReadOnlyPlaylist currentPlaylist) {
// On the first start, we remove all the songs from the playlist
// below the song
// we play.
int curPos = currentPlaylist.getPositionInList();
int numSongToRemove = currentPlaylist.getSongList().size() - curPos - 1;
PlayerControllerCommands changes = new PlayerControllerCommands();
for (int i = 0; i < numSongToRemove; i++) {
changes.removeSong(curPos + 1);
}
return changes;
}
@Override
public PlayModeType getPlayModeType() {
return PlayModeType.SIMILAR;
}
@Override
public PlayerControllerCommands next(IReadOnlyPlaylist currentPlaylist) throws NoNextSongException {
PlayerControllerCommands changes = new PlayerControllerCommands();
try {
PlaylistSong<BaseArtist, BaseAlbum> songToAdd;
if (currentPlaylist.isPlaylistEmpty()) {
songToAdd = getRandomSongWithCoordinates();
changes.addSong(songToAdd, 0);
changes.setListPos(0);
} else if (!isPlaylistAtEnd(currentPlaylist)) {
Integer nextPos = currentPlaylist.getPositionInList() + 1;
changes.setListPos(nextPos);
} else {
songToAdd = getSimilarSong(currentPlaylist);
Integer curPos = currentPlaylist.getPositionInList() + 1;
changes.addSong(songToAdd, curPos);
changes.setListPos(curPos);
}
changes.playerAction(PlayerAction.PLAY);
return changes;
} catch (Exception e) {
Log.w(TAG, e);
throw new NoNextSongException(e.getMessage(), e);
}
}
private boolean isPlaylistAtEnd(IReadOnlyPlaylist currentPlaylist) {
return currentPlaylist.getPositionInList() >= currentPlaylist.getSize() - 1;
}
private PlaylistSong<BaseArtist, BaseAlbum> getRandomSongWithCoordinates() throws NoNextSongException {
try {
PlaylistSong<BaseArtist, BaseAlbum> song = null;
int loopCnt = 0;
boolean checkArtist = similarArtistAvoidanceNumber > 0;
do {
List<PlaylistSong<BaseArtist, BaseAlbum>> songs = collectionModel.getSongProvider()
.getRandomSongWithCoordinates(1);
if (songs == null || songs.size() == 0) {
throw new NoNextSongException("No random song with coordinates available!");
}
song = songs.get(0);
// getRandomSong is only called for empty playlists, thus we can
// pass -1 as current song id to isForbiddenSong
} while (isForbiddenSong(null, song, checkArtist) && loopCnt++ < 10);
return song;
} catch (DataUnavailableException e) {
throw new NoNextSongException(e.getMessage(), e);
} catch (Exception e) {
Log.w(TAG, e);
throw new NoNextSongException(e.getMessage(), e);
}
}
private boolean isForbiddenSong(BaseSong<BaseArtist, BaseAlbum> currentSong,
BaseSong<BaseArtist, BaseAlbum> candidate, boolean checkArtist) {
if (currentSong != null && currentSong.getId() == candidate.getId()) {
return true;
}
if (playerModel.getPlayLog().isSongInRecentHistory(candidate, equalSongAvoidanceNumber)) {
return true;
}
if (checkArtist) {
int artistId = candidate.getArtist().getId();
if (currentSong != null && currentSong.getArtist().getId() == artistId) {
return true;
}
if (playerModel.getPlayLog().isArtistInRecentHistory(candidate.getArtist(), similarArtistAvoidanceNumber)) {
return true;
}
}
return false;
}
private PlaylistSong<BaseArtist, BaseAlbum> getSimilarSong(IReadOnlyPlaylist playlist) throws NoNextSongException {
try {
float[] meanPos = computeSeedPosition(playlist.getPositionInList(), playlist);
PlaylistSong<BaseArtist, BaseAlbum> song = getCloseSong(playlist, meanPos);
return song;
} catch (PlaylistPositionOutOfRangeException e) {
throw new NoNextSongException();
}
}
private float[] computeSeedPosition(int currentPosition, IReadOnlyPlaylist playlist)
throws PlaylistPositionOutOfRangeException {
BaseSong<BaseArtist, BaseAlbum> currentSong = playlist.getSongAtPosition(currentPosition);
if (seedSong == null) {
seedSong = playlist.getSongAtPosition(0);
}
float[] seedSongCoords;
try {
// TODO: should we save seedCoords instead of seedSong??
seedSongCoords = collectionModel.getOtherDataProvider().getSongCoordinates(seedSong);
} catch (DataUnavailableException e1) {
seedSongCoords = new float[Constants.DIM];
}
float[] currentSongCoords;
try {
currentSongCoords = collectionModel.getOtherDataProvider().getSongCoordinates(currentSong);
} catch (DataUnavailableException e1) {
currentSongCoords = new float[Constants.DIM];
}
float[] meanPos = Utils.getMean(seedSongCoords, currentSongCoords);
return meanPos;
}
private PlaylistSong<BaseArtist, BaseAlbum> getCloseSong(IReadOnlyPlaylist playlist, float[] meanPos)
throws NoNextSongException {
int numCandidates = NUM_CANDIDATES;
PlaylistSong<BaseArtist, BaseAlbum> currentSong = getCurrentSongOrNull(playlist);
// int currentSongId = currentSong == null ? -1 : currentSong.getId();
PlaylistSong<BaseArtist, BaseAlbum> song = null;
int loopCnt = -1;
boolean checkArtist = similarArtistAvoidanceNumber > 0;
do {
loopCnt++;
List<BaseSong<BaseArtist, BaseAlbum>> candidates;
try {
candidates = collectionModel.getSongProvider().getClosestBaseSongsToPosition(meanPos, numCandidates);
} catch (DataUnavailableException e) {
// TODO: inform that similar song mode is not yet available
throw new NoNextSongException();
}
Collections.shuffle(candidates);
Utils.printSongCollection("candidates:", candidates);
Log.v("Utils", "currentSongId: " + (currentSong == null ? null : currentSong.getId()));
for (BaseSong<BaseArtist, BaseAlbum> candidate : candidates) {
// Utils.printCollection("recent song history:",
// recentSongHistory);
if (!isForbiddenSong(currentSong, candidate, checkArtist)) {
song = new PlaylistSong<BaseArtist, BaseAlbum>(candidate, SongSource.SIMILAR_PLAY_MODE);
break;
}
}
if (song == null) {
numCandidates *= 2;
}
} while (song == null && loopCnt <= 7);
// TODO: handle song == null here!
return song;
}
private PlaylistSong<BaseArtist, BaseAlbum> getCurrentSongOrNull(IReadOnlyPlaylist playlist) {
try {
return playlist.getSongAtPosition(playlist.getPositionInList());
} catch (PlaylistPositionOutOfRangeException e) {
return null;
}
}
}