/*
* 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.model.player.playlog;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;
import ch.ethz.dcg.jukefox.commons.Constants;
import ch.ethz.dcg.jukefox.commons.DataUnavailableException;
import ch.ethz.dcg.jukefox.commons.DataWriteException;
import ch.ethz.dcg.jukefox.commons.utils.JoinableThread;
import ch.ethz.dcg.jukefox.commons.utils.Log;
import ch.ethz.dcg.jukefox.controller.player.IOnPlayerStateChangeListener;
import ch.ethz.dcg.jukefox.controller.player.IReadOnlyPlayerController;
import ch.ethz.dcg.jukefox.data.HttpHelper;
import ch.ethz.dcg.jukefox.data.context.AbstractContextResult;
import ch.ethz.dcg.jukefox.data.context.IContextProvider;
import ch.ethz.dcg.jukefox.data.db.IDbDataPortal;
import ch.ethz.dcg.jukefox.manager.ModelSettingsManager;
import ch.ethz.dcg.jukefox.model.TagPlaylistGenerator;
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.CompleteTag;
import ch.ethz.dcg.jukefox.model.collection.DateTag;
import ch.ethz.dcg.jukefox.model.collection.PlaylistSong;
import ch.ethz.dcg.jukefox.model.collection.PlaylistSong.SongSource;
import ch.ethz.dcg.jukefox.model.player.PlayerState;
import ch.ethz.dcg.jukefox.model.providers.SongProvider;
import ch.ethz.dcg.jukefox.model.providers.TagProvider;
import ch.ethz.dcg.jukefox.model.rating.RatingHelper;
public class PlayLog implements IOnPlayerStateChangeListener {
private final static String TAG = PlayLog.class.getSimpleName();
private final IContextProvider contextProvider;
private final int playerModelId;
private final SongProvider songProvider;
private final TagProvider tagProvider;
private final TagPlaylistGenerator tagPlaylistGenerator;
private final IDbDataPortal dbDataPortal;
private final ModelSettingsManager modelSettingsManager;
private final RatingHelper ratingHelper;
private int lastReturnedLogId;
private IReadOnlyPlayerController playerController;
/**
* Creates a new instance of {@link PlayLog}
*/
public PlayLog(IContextProvider contextProvider, int playerModelId, SongProvider songProvider,
TagProvider tagProvider, TagPlaylistGenerator tagPlaylistGenerator, IDbDataPortal dbDataPortal,
ModelSettingsManager modelSettingsManager, RatingHelper ratingHelper) {
this.contextProvider = contextProvider;
this.playerModelId = playerModelId;
this.songProvider = songProvider;
this.tagProvider = tagProvider;
this.modelSettingsManager = modelSettingsManager;
this.tagPlaylistGenerator = tagPlaylistGenerator;
this.dbDataPortal = dbDataPortal;
this.ratingHelper = ratingHelper;
}
/*
* (non-Javadoc)
*
* @see
* ch.ethz.dcg.jukefox.model.player.playlog.asdfasdf#writeToPlayLogAsync
* (int, java.util.Date, ch.ethz.dcg.jukefox.model.collection.PlaylistSong,
* boolean, int)
*/
public void writeToPlayLogAsync(final Date time, final PlaylistSong<BaseArtist, BaseAlbum> song,
final boolean skip, final int playbackPosition) {
new Thread(new Runnable() {
@Override
public void run() {
writeToPlayLog(time, song, skip, playbackPosition);
}
}).start();
}
public void writeToPlayLog(final Date time, final PlaylistSong<BaseArtist, BaseAlbum> song,
final boolean skip, final int playbackPosition) {
int day_of_week = 0, hour_of_day = 0;
long utcTime = time.getTime();
Calendar cal = Calendar.getInstance();
// in minutes
int timeZoneOffset = (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / 60000;
// in hours
timeZoneOffset /= 60;
Calendar logcalendar = Calendar.getInstance();
logcalendar.setTimeInMillis(time.getTime());
day_of_week = logcalendar.get(Calendar.DAY_OF_WEEK);
hour_of_day = logcalendar.get(Calendar.HOUR_OF_DAY);
Log.v(TAG, "day of week: " + day_of_week);
Log.v(TAG, "hour of day: " + hour_of_day);
Log.v(TAG, "utcTime: " + utcTime);
AbstractContextResult contextData = contextProvider.getMeanContextValues(30 * 1000);
try {
// Write playlog entry
dbDataPortal.writePlayLogEntry(playerModelId, song, utcTime, timeZoneOffset, day_of_week, hour_of_day,
skip, playerController.getPlayMode().getPlayModeType().value(), contextData, playbackPosition);
// Write a rating entry as well
double fractionPlayed = playbackPosition / (double) song.getDuration();
ratingHelper.addRatingFromPlayLog(song, time, fractionPlayed);
} catch (DataWriteException e) {
Log.w(TAG, e);
}
}
/*
* (non-Javadoc)
*
* @see
* ch.ethz.dcg.jukefox.model.player.playlog.asdfasdf#setSentSuccessful()
*/
public void setSentSuccessful() {
Log.v(TAG, "Sending log succeded: Saving last Sent Id");
modelSettingsManager.setLastSentPlayLogId(lastReturnedLogId);
}
/*
* (non-Javadoc)
*
* @see ch.ethz.dcg.jukefox.model.player.playlog.asdfasdf#sendPlayLog()
*/
public void sendPlayLog() {
if (!modelSettingsManager.isHelpImproveJukefox()) {
return;
}
JoinableThread t = new JoinableThread(new Runnable() {
@Override
public void run() {
// Log.v(TAG, "SendPlaylog");
int coordinateVersion = modelSettingsManager.getCoordinateVersion();
long lastSentId = modelSettingsManager.getLastSentPlayLogId();
PlayLogSendEntity logEntity = null;
try {
logEntity = dbDataPortal.getPlayLogString(playerModelId, Constants.PLAY_LOG_VERSION,
coordinateVersion, lastSentId);
} catch (DataUnavailableException e) {
Log.w(TAG, e);
}
if (logEntity == null) {
return;
}
String log = logEntity.logString;
lastReturnedLogId = logEntity.lastId;
// Log.v(TAG, log);
if (log == null || log.equals("")) {
return;
}
String answer = null;
try {
// Create a new HttpClient and Post Header
HttpClient httpClient = HttpHelper.createHttpClientWithDefaultSettings();
HttpPost httpPost = new HttpPost(Constants.FORMAT_PLAY_LOG_URL);
httpPost.setHeader(HTTP.CONTENT_TYPE, "application/x-www-form-urlencoded");
httpPost.setEntity(new StringEntity("log=" + log));
// Execute HTTP Post Request
HttpResponse response = httpClient.execute(httpPost);
answer = EntityUtils.toString(response.getEntity());
// Log.v(TAG, "playLog sent, answer: " + answer);
} catch (Exception e) {
Log.w(TAG, e);
answer = null;
}
if (answer != null) {
Log.v(TAG, "Play Log Server answer: " + answer);
if (answer.equals("1")) {
setSentSuccessful();
}
}
}
});
t.start();
}
/*
* (non-Javadoc)
*
* @see ch.ethz.dcg.jukefox.model.player.playlog.asdfasdf#
* getArbitrarySongForDateRange(long, long)
*/
public PlaylistSong<BaseArtist, BaseAlbum> getArbitrarySongForDateRange(long fromTimestamp, long toTimestamp)
throws DataUnavailableException {
return new PlaylistSong<BaseArtist, BaseAlbum>(songProvider.getArbitraryBaseSongInTimeRange(playerModelId,
fromTimestamp, toTimestamp), SongSource.TIME_BASED);
}
/*
* (non-Javadoc)
*
* @see
* ch.ethz.dcg.jukefox.model.player.playlog.asdfasdf#getSongCloseToDateRange
* (long, long, float, float)
*/
public PlaylistSong<BaseArtist, BaseAlbum> getSongCloseToDateRange(long fromTimestamp, long toTimestamp,
float toleranceRange, float toleranceGlobal) throws DataUnavailableException {
return new PlaylistSong<BaseArtist, BaseAlbum>(songProvider.getBaseSongCloseToTimeRange(playerModelId,
fromTimestamp, toTimestamp, toleranceRange, toleranceGlobal), SongSource.TIME_BASED);
}
/*
* (non-Javadoc)
*
* @see
* ch.ethz.dcg.jukefox.model.player.playlog.asdfasdf#getSongsForDateRange
* (long, long, int)
*/
public List<PlaylistSong<BaseArtist, BaseAlbum>> getSongsForDateRange(long fromTimestamp, long toTimestamp,
int number) {
return songProvider.getPlaylistSongsForTimeRange(playerModelId, fromTimestamp, toTimestamp, number);
}
/*
* (non-Javadoc)
*
* @see
* ch.ethz.dcg.jukefox.model.player.playlog.asdfasdf#getSongByDateTag(long,
* long)
*/
public List<PlaylistSong<BaseArtist, BaseAlbum>> getSongByDateTag(long fromTimestamp, long toTimestamp)
throws DataUnavailableException {
ArrayList<DateTag> sortedDateTags = tagProvider.getSortedDateTags();
long avgTime = (fromTimestamp + toTimestamp) / 2;
DateTag bestTag = null;
for (DateTag t : sortedDateTags) {
if (toTimestamp < t.getFrom()) {
continue;
}
if (fromTimestamp > t.getTo()) {
continue;
}
// we have at least some overlap...
if (bestTag == null) {
bestTag = t;
continue;
}
long bestDiff = Math.abs(bestTag.getTime() - avgTime);
long curDiff = Math.abs(t.getTime() - avgTime);
if (curDiff < bestDiff) {
bestTag = t;
continue;
}
if (curDiff == bestDiff && t.getRange() < bestTag.getRange()) {
bestTag = t;
}
}
if (bestTag != null) {
return returnSongsForDateTag(bestTag);
}
// no tag overlaps with our time range... just take the one with best
// mean-time fit...
DateTag relevantDate = new DateTag();
relevantDate.setFrom(fromTimestamp);
relevantDate.setTo(toTimestamp);
int idx = Collections.binarySearch(sortedDateTags, relevantDate, new Comparator<DateTag>() {
@Override
public int compare(DateTag t1, DateTag t2) {
if (t1.getTime() < t2.getTime()) {
return -1;
}
if (t1.getTime() > t2.getTime()) {
return 1;
}
return 0;
}
});
if (idx > 0) {
DateTag tag = sortedDateTags.get(idx);
return returnSongsForDateTag(tag);
}
idx = Math.min(-idx - 1, sortedDateTags.size() - 1);
DateTag tag = sortedDateTags.get(idx);
return returnSongsForDateTag(tag);
}
private List<PlaylistSong<BaseArtist, BaseAlbum>> returnSongsForDateTag(DateTag bestTag)
throws DataUnavailableException {
CompleteTag tag = tagProvider.getCompleteTag(bestTag.getId());
Log.v(TAG, "getting songs for tag: " + tag.getName());
return tagPlaylistGenerator.generatePlaylist(tag, TagPlaylistGenerator.DEFAULT_PLAYLIST_SIZE,
TagPlaylistGenerator.DEFAULT_SAMPLE_FACTOR);
}
/**
* Is the given {@link BaseSong} in the recent history?
*
* @param candidate
* The {@link BaseSong}
* @param equalSongAvoidanceNumber
* @return
*/
public boolean isSongInRecentHistory(BaseSong<BaseArtist, BaseAlbum> candidate, int equalSongAvoidanceNumber) {
boolean result = false;
try {
result = dbDataPortal.isSongInRecentHistory(playerModelId, candidate, equalSongAvoidanceNumber);
} catch (DataUnavailableException e) {
Log.w(TAG, e);
}
return result;
}
/**
*
* @param artist
* @param similarArtistAvoidanceNumber
* @return
*/
public boolean isArtistInRecentHistory(BaseArtist baseArtist, int similarArtistAvoidanceNumber) {
boolean result = false;
try {
result = dbDataPortal.isArtistInRecentHistory(playerModelId, baseArtist, similarArtistAvoidanceNumber);
} catch (DataUnavailableException e) {
Log.w(TAG, e);
}
return result;
}
public void setPlayerController(IReadOnlyPlayerController playerController) {
this.playerController = playerController;
playerController.addOnPlayerStateChangeListener(this);
}
@Override
public void onPlayerStateChanged(PlayerState playerState) {
}
@Override
public void onSongCompleted(PlaylistSong<BaseArtist, BaseAlbum> song) {
writeToPlayLogAsync(new Date(), song, false, song.getDuration());
}
@Override
public void onSongSkipped(PlaylistSong<BaseArtist, BaseAlbum> song, int position) {
writeToPlayLogAsync(new Date(), song, true, position);
}
@Override
public void onSongStarted(PlaylistSong<BaseArtist, BaseAlbum> song) {
}
}