/* * 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 ch.ethz.dcg.jukefox.commons.utils.Utils; 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.playmode.smartshuffle.NextSongCalculationThread.Case; public class NextSongCalculationThreadManager extends Thread { private final Helper helper; private NextSongCalculationThread runningCalculation = null; private BaseSong<BaseArtist, BaseAlbum> currentlyPlayingSong = null; private NextSongCalculationThread lastFinishedNegative = null; private NextSongCalculationThread lastFinishedPositive = null; private NextSongCalculationThread nextMostUpToDate = null; private NextSongCalculationThread mostUpToDate = null; public NextSongCalculationThreadManager(Helper helper) { super(); setDaemon(true); setName(NextSongCalculationThreadManager.class.getSimpleName()); this.helper = helper; } @Override public void run() { super.run(); for (;;) { boolean runningChanged = false; synchronized (this) { // Evaluate what calculation should be calculated next Case nextCase = getNextCase(); if (nextCase == Case.Positive) { runningCalculation = helper.createPositiveNextSongCalculationThread(currentlyPlayingSong); runningChanged = true; } else if (nextCase == Case.Negative) { runningCalculation = helper.createNegativeNextSongCalculationThread(currentlyPlayingSong); runningChanged = true; } if (!runningChanged) { try { // Nothing to do -> wait until we need to work again wait(); } catch (InterruptedException e) { // Just ignore the exception. This does not affect us in any way. } } } if (runningChanged) { // Start the calculation runningCalculation.start(); // Wait for the calculation thread to finish while (runningCalculation.isAlive()) { try { runningCalculation.realJoin(); } catch (InterruptedException e) { // Just ignore the exception. } } if (!runningCalculation.isAborted()) { synchronized (this) { if (runningCalculation.getCalculationCase() == Case.Positive) { lastFinishedPositive = runningCalculation; } else { lastFinishedNegative = runningCalculation; } if (runningCalculation.equals(nextMostUpToDate) || (mostUpToDate == null)) { mostUpToDate = runningCalculation; } } } } } } /** * Returns which case the next calculation should cover to be most likely right. If we do not need to do anything in * the moment, <code>null</code> is returned. * * @return The next case */ private Case getNextCase() { // Check if song changed boolean differentSong = true; if (runningCalculation != null) { BaseSong<BaseArtist, BaseAlbum> calculationSong = runningCalculation.getCurrentSong(); differentSong = !Utils.nullEquals(currentlyPlayingSong, calculationSong); } // Evaluate the situation double rating = helper.getTemporaryRatingForSong(currentlyPlayingSong); Case nextCase; if (!differentSong) { // Still the same song as when we started the last calculation thread if (runningCalculation.getCalculationCase() == Case.Negative) { // Last was negative -> start positive calculation nextCase = Case.Positive; } else { // no work is needed at the moment nextCase = null; } } else { // New situation -> check how much of the current song is already played if (rating >= 0) { // Start positive lookahead calculation nextCase = Case.Positive; } else { // Start negative lookahead calculation nextCase = Case.Negative; } } return nextCase; } /** * Adjusts the most up-to-date next song prediction based on the rating of the just finished song. * * @param song * The song that just finished * @param rating * The rating of the song that just finished */ public synchronized void songFinished(BaseSong<BaseArtist, BaseAlbum> song, double rating) { // Find the calculations which considered this song NextSongCalculationThread finishedNegative = null; NextSongCalculationThread finishedPositive = null; NextSongCalculationThread running = null; if ((lastFinishedNegative != null) && Utils.nullEquals(lastFinishedNegative.getCurrentSong(), song)) { finishedNegative = lastFinishedNegative; } if ((lastFinishedPositive != null) && Utils.nullEquals(lastFinishedPositive.getCurrentSong(), song)) { finishedPositive = lastFinishedPositive; } if ((runningCalculation != null) && Utils.nullEquals(runningCalculation.getCurrentSong(), song) && runningCalculation .isAlive()) { running = runningCalculation; } boolean upToDateChanged = false; if (rating >= 0) { // Positive rating if (finishedPositive != null) { mostUpToDate = finishedPositive; upToDateChanged = true; } else if (finishedNegative != null) { mostUpToDate = finishedNegative; upToDateChanged = true; } } else { // Negative rating if (finishedNegative != null) { mostUpToDate = finishedNegative; upToDateChanged = true; } else if (finishedPositive != null) { mostUpToDate = finishedPositive; upToDateChanged = true; } } if (running != null) { if (upToDateChanged) { // We found a correct predicted calculation -> dont waste time on this outdated calculation running.abortCalculation(); } else { nextMostUpToDate = running; // Will become the most up-to-date calculation once it finishes } } } /** * Adjusts the calculation behavior * * @param currentSong */ public synchronized void currentSongChanged(BaseSong<BaseArtist, BaseAlbum> currentSong) { currentlyPlayingSong = currentSong; notifyAll(); // Inform, that new work arrived } /** * Returns the most up-to-date {@link NextSongCalculationThread}. Please make sure, that you called * {@link #songFinished(BaseSong, double)} before this to get the best result. * * @return The most up-to-date {@link NextSongCalculationThread} */ public synchronized NextSongCalculationThread getNextSongCalculationThread() { return mostUpToDate; } /** * Helper for the {@link NextSongCalculationThreadManager}. This ensures the information hiding principle. */ public interface Helper { public PositiveNextSongCalculationThread createPositiveNextSongCalculationThread( BaseSong<BaseArtist, BaseAlbum> currentSong); public NegativeNextSongCalculationThread createNegativeNextSongCalculationThread( BaseSong<BaseArtist, BaseAlbum> currentSong); /** * Returns the rating out of the actual playback position of the given song. If the given song is not played at * the moment, <code>-1</code> is returned. * * @param song * The song * @return The temporary rating */ public double getTemporaryRatingForSong(BaseSong<BaseArtist, BaseAlbum> song); } }