/* * 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.smartshuffle; import java.util.LinkedList; import java.util.List; import java.util.Map; import ch.ethz.dcg.jukefox.commons.DataUnavailableException; import ch.ethz.dcg.jukefox.commons.utils.JoinableThread; import ch.ethz.dcg.jukefox.commons.utils.Log; import ch.ethz.dcg.jukefox.commons.utils.Pair; import ch.ethz.dcg.jukefox.commons.utils.RandomProvider; import ch.ethz.dcg.jukefox.commons.utils.Utils; import ch.ethz.dcg.jukefox.data.log.SmartShuffleNextSongLogEntry; import ch.ethz.dcg.jukefox.data.log.SmartShuffleNextSongLogEntry.SongSource; 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.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; import ch.ethz.dcg.jukefox.model.rating.RatingHelper; import ch.ethz.dcg.jukefox.playmode.BasePlayMode; import ch.ethz.dcg.jukefox.playmode.PlayerControllerCommands; import ch.ethz.dcg.jukefox.playmode.smartshuffle.agents.AgentManager; import ch.ethz.dcg.jukefox.playmode.smartshuffle.agents.IAgent; public class SmartShufflePlayMode2 extends BasePlayMode { private final static String TAG = SmartShufflePlayMode2.class.getSimpleName(); private final AgentManager agentManager; private IReadOnlyPlaylist playlist = null; private final List<AgentWeightsAdjustmentThread> agentWeightAdjustmentThreads = new LinkedList<AgentWeightsAdjustmentThread>(); private int negativeOfSameCalculatorCount = 0; private int lastNextSongCalculatorPosition = 0; private NextSongCalculationThread lastNextSongCalculator = null; private final NextSongCalculationThreadManager nextSongCalculationThreadManager; private Pair<BaseSong<BaseArtist, BaseAlbum>, SmartShuffleNextSongLogEntry.Builder> currentLog = null; public SmartShufflePlayMode2(AbstractCollectionModelManager collectionModel, AbstractPlayerModelManager playerModel) { super(collectionModel, playerModel); agentManager = AgentManager.initialize(collectionModel.getDbDataPortal(), collectionModel.getSongProvider(), playerModel.getStatisticsProvider()); nextSongCalculationThreadManager = new NextSongCalculationThreadManager(nextSongCalculationThreadManagerHelper); } /** * Clears the playlist from the current position to the end and starts the next song calculation. */ @Override public PlayerControllerCommands initialize(IReadOnlyPlaylist currentPlaylist) { // Start the next song calculation nextSongCalculationThreadManager.currentSongChanged(getCurrentSong(currentPlaylist)); nextSongCalculationThreadManager.start(); // Remove songs which are further in the list as our current position PlayerControllerCommands commands = new PlayerControllerCommands(); for (int i = currentPlaylist.getSize() - 1; i > currentPlaylist.getPositionInList(); --i) { commands.removeSong(i); } return commands; } /** * Reads out the next song from the asynchronous calculation threads and starts new ones for the next song.<br/> * If the time for caluclating the next song was too short (very early skip), an older calculation will be * considered or if none exists, a random song will be picked. */ @Override public synchronized PlayerControllerCommands next(IReadOnlyPlaylist playlist) throws NoNextSongException { this.playlist = playlist; // Get the finished song and its final rating BaseSong<BaseArtist, BaseAlbum> finishedSong = getCurrentSong(playlist); double rating = getCurrentRating(playlist); // Inform the nextSongCalculationThreadManager about the finished song nextSongCalculationThreadManager.songFinished(finishedSong, rating); // Write the log entry if ((currentLog != null) && Utils.nullEquals(currentLog.first, finishedSong)) { // Get finished meSongId Integer finishedMeSongId = null; try { finishedMeSongId = collectionModel.getOtherDataProvider().getMusicExplorerSongId(finishedSong); } catch (DataUnavailableException e) { Log.w(TAG, e); } SmartShuffleNextSongLogEntry.Builder log = currentLog.second; log .setCurrentMeSongId((finishedMeSongId != null) ? finishedMeSongId : 0) .setCurrentRating(rating) .setFractionPlayed(getCurrentFraction(playlist)) .setSecondsPlayed(getMilliSecondsPlayed(playlist) / 1000); playerModel.getLogManager().addLogEntry(log.build()); } // Start new log entry SmartShuffleNextSongLogEntry.Builder log = SmartShuffleNextSongLogEntry.createInstance(); // Adjust the agent weights if ((lastNextSongCalculator != null) && (lastNextSongCalculatorPosition == 0)) { // Do it async to ensure no delays in this method (i.e. in case of db locks) AgentWeightsAdjustmentThread awat = new AgentWeightsAdjustmentThread(getAgentVotesForCurrentSong(), (float) rating); agentWeightAdjustmentThreads.add(awat); awat.start(); } // Choose the next song PlaylistSong<BaseArtist, BaseAlbum> nextSong = null; PlayerControllerCommands commands = new PlayerControllerCommands(); if (playlist.getPositionInList() < (playlist.getSize() - 1)) { // We are in the middle of the playlist -> just jump to the next song commands.setListPos(playlist.getPositionInList() + 1); log.setSongSource(SongSource.Playlist); // Adjust currentSong try { nextSong = playlist.getSongAtPosition(playlist.getPositionInList() + 1); lastNextSongCalculator = null; } catch (PlaylistPositionOutOfRangeException e) { Log.w(TAG, e); assert false; } } else { // We are at the end of the playlist // Get the most up-to-date NextSongCalculationThread NextSongCalculationThread nextSongCalculationThread = nextSongCalculationThreadManager .getNextSongCalculationThread(); if (Utils.nullEquals(lastNextSongCalculator, nextSongCalculationThread)) { Log.d(TAG, "Getting song from ALREADY USED lastNextSongCalculator"); ++lastNextSongCalculatorPosition; } else { Log.d(TAG, "Getting song from NEW lastNextSongCalculator"); lastNextSongCalculator = nextSongCalculationThread; lastNextSongCalculatorPosition = 0; negativeOfSameCalculatorCount = 0; } if (rating < 0) { ++negativeOfSameCalculatorCount; } // Get the next song & agent votes if (nextSongCalculationThread != null) { if (negativeOfSameCalculatorCount == 0) { // First entry of this calculation thread -> just use the proposal log.setSongSource(SongSource.NextCalculator); nextSong = nextSongCalculationThread.getProposedSong(lastNextSongCalculatorPosition); } else if (negativeOfSameCalculatorCount < 3) { // We are reusing this calculation thread, but acceptance rate is good enough log.setSongSource(SongSource.ReusedNextCalculator); BaseArtist lastArtist = finishedSong.getArtist(); --lastNextSongCalculatorPosition; // cancel ++ on first run do { ++lastNextSongCalculatorPosition; nextSong = nextSongCalculationThread.getProposedSong(lastNextSongCalculatorPosition); } while ((rating < 0) && Utils.nullEquals(lastArtist, nextSong.getArtist()) && (nextSong != null)); // Enforce that an artist is not played twice in a row if it was rated negative if (nextSong == null) { Log.d(TAG, "Using random song, since NextSongCalculation has no song left in its list."); } } else { // Too much negative proposals of the same calculator -> use random since it voted not that promising Log.d(TAG, "Using random song, since prediction is too bad."); log.setSongSource(SongSource.Random); nextSong = getRandomSong(playlist); } if (negativeOfSameCalculatorCount < 3) { log .setOptAgentVotes(nextSongCalculationThread.getAgentVotes(lastNextSongCalculatorPosition)) .setOptAgentWeights(nextSongCalculationThread.getAgentWeights()); } } if (nextSong == null) { // Choose random song if (nextSongCalculationThread == null) { Log.w(TAG, "Ran out of time - had to choose a random song"); } else { Log.d(TAG, "We are choosing a random song (a NextSongCalculation is around)"); } log.setSongSource(SongSource.Random); nextSong = getRandomSong(playlist); } if (nextSong == null) { // We are fu*** up.. How should we recover? Just play one song from the playlist once more... log.setSongSource(SongSource.Random); nextSong = getRandomSongFromPlaylist(playlist); } if (nextSong != null) { // Add next song to the end of the playlist commands.addSong(nextSong, playlist.getSize()); commands.setListPos(playlist.getSize()); } } if (nextSong != null) { commands.playerAction(PlayerAction.PLAY); nextSongCalculationThreadManager.currentSongChanged(nextSong); currentLog = new Pair<BaseSong<BaseArtist, BaseAlbum>, SmartShuffleNextSongLogEntry.Builder>(nextSong, log); } else { throw new NoNextSongException(); } return commands; } /** * Returns a random song, which is not already in the given playlist. If after 10 attemps no song could be found, * null is returned. * * @param playlist * The playlist * @return The random song */ private PlaylistSong<BaseArtist, BaseAlbum> getRandomSong(IReadOnlyPlaylist playlist) { try { int i = 0; while (i < 10) { // Ensure termination PlaylistSong<BaseArtist, BaseAlbum> song = collectionModel.getSongProvider().getRandomSong(); if (playlist.getSongList().indexOf(song) == -1) { // This song is not in the playlist yet return song; } ++i; } } catch (DataUnavailableException e) { Log.w(TAG, e); } return null; } /** * Returns a random song from the given playlist. If the playlist is empty <code>null</code> is returned. * * @param playlist * The playlist * @return The random song */ private PlaylistSong<BaseArtist, BaseAlbum> getRandomSongFromPlaylist(IReadOnlyPlaylist playlist) { if (playlist.isPlaylistEmpty()) { return null; } int pos = RandomProvider.getRandom().nextInt(playlist.getSize()); try { return playlist.getSongAtPosition(pos); } catch (PlaylistPositionOutOfRangeException e) { // This really should never occur... assert false; return null; } } /** * Returns the currently played song (could be paused as well ;) ). Returns <code>null</code> if the playlist is * empty. * * @param playlist * The playlist * @return The currently played song */ private PlaylistSong<BaseArtist, BaseAlbum> getCurrentSong(IReadOnlyPlaylist playlist) { if ((playlist == null) || playlist.isPlaylistEmpty()) { return null; } try { return playlist.getSongAtPosition(playlist.getPositionInList()); } catch (PlaylistPositionOutOfRangeException e) { assert false; return null; } } /** * Returns the rating out of the actual playback position of the played song or <code>-1</code> if no such song * exists. * * @param playlist * The playlist * @return The temporary rating */ private double getCurrentRating(IReadOnlyPlaylist playlist) { return RatingHelper.getRatingFromFractionPlayed(getCurrentFraction(playlist)); } /** * Returns the playback-fraction of the current song or <code>0</code> if no such song exists. * * @param playlist * The playlist * @return The temporary fraction played */ private double getCurrentFraction(IReadOnlyPlaylist playlist) { BaseSong<BaseArtist, BaseAlbum> currentSong = getCurrentSong(playlist); if (currentSong != null) { return getMilliSecondsPlayed(playlist) / (double) currentSong.getDuration(); } else { return 0.0d; // We are at the start of the non-existing song } } /** * Returns the playback-position of the current song. * * @param playlist * The playlist * @return The temporary fraction played */ private int getMilliSecondsPlayed(IReadOnlyPlaylist playlist) { return playlist.getPositionInSong(); } /** * Returns the agent votes for the currently playing song. If no such ratings exist, <code>null</code> will be * returned. * * @return The agent votes for the currently playing song */ public Map<IAgent, Float> getAgentVotesForCurrentSong() { if (lastNextSongCalculator == null) { return null; } return lastNextSongCalculator.getAgentVotes(lastNextSongCalculatorPosition); } /** * @see NextSongCalculationThreadManager.Helper */ private final NextSongCalculationThreadManager.Helper nextSongCalculationThreadManagerHelper = new NextSongCalculationThreadManager.Helper() { @Override public double getTemporaryRatingForSong(BaseSong<BaseArtist, BaseAlbum> song) { if (Utils.nullEquals(getCurrentSong(playlist), song)) { return getCurrentRating(playlist); } else { return -1; } } @Override public PositiveNextSongCalculationThread createPositiveNextSongCalculationThread( BaseSong<BaseArtist, BaseAlbum> currentSong) { return new PositiveNextSongCalculationThread(currentSong, agentManager, collectionModel.getDbDataPortal(), playerModel.getPlayLog(), playerModel.getLogManager(), playerModel.getStatisticsProvider(), collectionModel.getOtherDataProvider()); } @Override public NegativeNextSongCalculationThread createNegativeNextSongCalculationThread( BaseSong<BaseArtist, BaseAlbum> currentSong) { return new NegativeNextSongCalculationThread(currentSong, agentManager, collectionModel.getDbDataPortal(), playerModel.getPlayLog(), playerModel.getLogManager(), playerModel.getStatisticsProvider(), collectionModel.getOtherDataProvider()); } }; @Override public PlayModeType getPlayModeType() { return PlayModeType.SMART_SHUFFLE; } //****************************** // CLASSES //******************************* private class AgentWeightsAdjustmentThread extends JoinableThread { private final Map<IAgent, Float> agentRatings; private final float realRating; public AgentWeightsAdjustmentThread(Map<IAgent, Float> agentRatings, float realRating) { this.agentRatings = agentRatings; this.realRating = realRating; } @Override public void run() { super.run(); agentManager.adjustAgentWeights(agentRatings, realRating); // Mark this work as done agentWeightAdjustmentThreads.remove(this); } } }