/*
* Jajuk
* Copyright (C) The Jajuk Team
* http://jajuk.info
*
* 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 2
* of the License, or 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, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
*/
package org.jajuk.services.players;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import javax.swing.JOptionPane;
import org.jajuk.base.Album;
import org.jajuk.base.Device;
import org.jajuk.base.Directory;
import org.jajuk.base.File;
import org.jajuk.base.FileManager;
import org.jajuk.events.JajukEvent;
import org.jajuk.events.JajukEvents;
import org.jajuk.events.ObservationManager;
import org.jajuk.services.core.ExitService;
import org.jajuk.services.core.SessionService;
import org.jajuk.services.webradio.WebRadio;
import org.jajuk.ui.helpers.JajukTimer;
import org.jajuk.util.Conf;
import org.jajuk.util.Const;
import org.jajuk.util.Messages;
import org.jajuk.util.UtilFeatures;
import org.jajuk.util.UtilGUI;
import org.jajuk.util.UtilString;
import org.jajuk.util.UtilSystem;
import org.jajuk.util.error.JajukException;
import org.jajuk.util.log.Log;
/**
* Manages playing sequences
* <p>
* Avoid to synchronize these methods because they are called very often and AWT
* dispatcher thread is frozen when JVM execute a static synchronized method,
* even outside AWT dispatcher thread
* </p>
*
* General todo-items:
*
* TODO: we catch exceptions a lot in various places here. Why? We should
* probably rather avoid or handle them correctly
*
* TODO: insert() and push() are quite similar, but implemented differently,
* they should be combined
*
* TOOD: the queue/planned handling is cumbersome and planned tracks are not
* correctly handled sometimes, should be refactored into separate class
*/
public final class QueueModel {
/** Currently played track index or -1 if none playing item. */
private static volatile int index = -1;
/** Last played track. */
static volatile StackItem itemLast;
/**
* The Fifo itself, contains jajuk File objects. This also includes an
* optional bunch of planned tracks which are accessible with separate
* methods.
*/
private static volatile QueueList queue = new QueueList();
/** Stop flag*. */
private static volatile boolean bStop = true;
/** First played file flag. */
private static boolean bFirstFile = true;
/** Whether we are currently playing radio. */
private static volatile boolean playingRadio = false;
/** Current played radio. */
private static volatile WebRadio currentRadio;
/** Last played track actually played duration in ms before a stop. */
private static long lastDuration;
/** Should be stop after current track playback ?. */
private static boolean bStopAfter;
/** Whether a required stop comes from jajuk */
private static boolean bInternalStop;
/**
* Gets the last duration.
*
* @return Last played track actually played duration in ms before a stop
*/
public static long getLastDuration() {
return lastDuration;
}
/**
* No constructor, this class is used statically only.
*/
private QueueModel() {
}
/**
* FIFO total re-initialization.
*
* Do not set itemLast to null as we need to keep this information in some
* places
*/
public static void reset() {
clear();
JajukTimer.getInstance().reset();
bStop = true;
playingRadio = false;
currentRadio = null;
}
/**
* Clears the fifo, for example when we want to add a group of files
* stopping previous plays.
*/
public static void clear() {
queue.clear();
index = -1;
queue.clearPlanned();
}
/**
* Remove current item (it is always removed independently of its album) and all items from the given album just before and after the given
* index, i.e. remove all tracks before and after the current one that have
* the same album.
*
* @param index
* The index from where to remove.
* @param album
* The album to remove.
*/
public static void resetAround(int index, Album album) {
Set<Integer> indexesToRemove = new TreeSet<Integer>();
// Add provided index, this one is always removed
indexesToRemove.add(index);
for (int i = index; i >= 0; i--) {
if (queue.get(i).getFile().getTrack().getAlbum().equals(album)) {
indexesToRemove.add(i);
}
}
for (int i = index; i < queue.size(); i++) {
if (queue.get(i).getFile().getTrack().getAlbum().equals(album)) {
indexesToRemove.add(i);
}
}
remove(indexesToRemove);
}
/**
* Set given repeat mode to all in FIFO.
*
* @param bRepeat
* True, if repeat mode should be turned on, false otherwise.
*/
public static void setRepeatModeToAll(boolean bRepeat) {
for (StackItem item : queue) {
item.setRepeat(bRepeat);
}
}
/**
* Asynchronous version of push (needed to perform long-task out of awt
* dispatcher thread).
*
* @param alItems
* The list of items to push.
* @param bKeepPrevious
* keep previous files or stop them to start a new one ?
*/
public static void push(final List<StackItem> alItems, final boolean bKeepPrevious) {
push(alItems, bKeepPrevious, false);
}
/**
* Asynchronous version of push (needed to perform long-task out of awt
* dispatcher thread).
*
* @param alItems
* The list of items to push.
* @param bKeepPrevious
* keep previous files or stop them to start a new one ?
* @param bPushNext
* whether the selection is added after playing track (mutual
* exclusive with simple push)
*/
public static void push(final List<StackItem> alItems, final boolean bKeepPrevious,
final boolean bPushNext) {
Thread t = new Thread("Queue Push Thread") { // do it in a thread to
// make
// UI more reactive
@Override
public void run() {
try {
UtilGUI.waiting();
pushCommand(alItems, bKeepPrevious, bPushNext);
} catch (Exception e) {
Log.error(e);
} finally {
// refresh playlist editor
ObservationManager.notify(new JajukEvent(JajukEvents.QUEUE_NEED_REFRESH));
UtilGUI.stopWaiting();
}
}
};
t.setPriority(Thread.MAX_PRIORITY);
t.start();
}
/**
* Asynchronous version of push (needed to perform long-task out of awt
* dispatcher thread).
*
* @param item
* The item to push.
* @param bKeepPrevious
* keep previous files or stop them to start a new one ?
*/
public static void push(final StackItem item, final boolean bKeepPrevious) {
push(item, bKeepPrevious, false);
}
/**
* Asynchronous version of push (needed to perform long-task out of awt
* dispatcher thread).
*
* @param item
* The item to push.
* @param bKeepPrevious
* keep previous files or stop them to start a new one ?
* @param bPushNext
* whether the selection is added after playing track (mutual
* exclusive with simple push)
*/
public static void push(final StackItem item, final boolean bKeepPrevious, final boolean bPushNext) {
Thread t = new Thread("Queue Push Thread") {
// do it in a thread to make UI more reactive
@Override
public void run() {
try {
UtilGUI.waiting();
pushCommand(item, bKeepPrevious, bPushNext);
} catch (Exception e) {
Log.error(e);
} finally {
// refresh queue
ObservationManager.notify(new JajukEvent(JajukEvents.QUEUE_NEED_REFRESH));
UtilGUI.stopWaiting();
}
}
};
t.setPriority(Thread.MAX_PRIORITY);
t.start();
}
/**
* Launch a web radio.
*
* @param radio
* webradio to launch
*/
public static void launchRadio(WebRadio radio) {
try {
UtilGUI.waiting();
/**
* Force buttons to opening mode by default, then if they start
* correctly, a PLAYER_PLAY event will be notified to update to
* final state. We notify synchronously to make sure the order
* between these two events will be correct*
*/
ObservationManager.notifySync(new JajukEvent(JajukEvents.PLAY_OPENING));
currentRadio = radio;
// Play the stream
boolean bPlayOK = Player.play(radio);
if (bPlayOK) { // refresh covers if play is started
Log.debug("Now playing :" + radio.toString());
playingRadio = true;
// Store current radio for next startup
Conf.setProperty(Const.CONF_DEFAULT_WEB_RADIO, radio.getName());
// Send an event that a webradio has been launched
Properties pDetails = new Properties();
pDetails.put(Const.DETAIL_CONTENT, radio);
ObservationManager.notify(new JajukEvent(JajukEvents.WEBRADIO_LAUNCHED, pDetails));
//If Webradio info had been updated for current station by WebRadioPlayerImpl then notify again with the updated info
Properties webradioInfoUpdatedEvent = ObservationManager
.getDetailsLastOccurence(JajukEvents.WEBRADIO_INFO_UPDATED);
if (webradioInfoUpdatedEvent != null) {
WebRadio updatedWebRadio = (WebRadio) webradioInfoUpdatedEvent.get(Const.DETAIL_CONTENT);
if (radio.getName().equals(updatedWebRadio.getName())) {
ObservationManager.notify(new JajukEvent(JajukEvents.WEBRADIO_INFO_UPDATED,
webradioInfoUpdatedEvent));
}
}
bStop = false;
}
} catch (Throwable t) {// catch even Errors (OutOfMemory for example)
Log.error(122, t);
playingRadio = false;
} finally {
UtilGUI.stopWaiting(); // stop the waiting cursor
}
}
/**
* Push some files in the fifo.
*
* @param item
* , item to be played
* @param bKeepPrevious
* keep previous files or stop them to start a new one ?
* @param bPushNext
* whether the selection is added after playing track (mutual
* exclusive with simple push)
*/
private static void pushCommand(StackItem item, boolean bKeepPrevious, final boolean bPushNext) {
List<StackItem> alFiles = new ArrayList<StackItem>(1);
alFiles.add(item);
pushCommand(alFiles, bKeepPrevious, bPushNext);
}
/**
* Push some stack items in the fifo.
*
* @param alItems
* , list of items to be played
* @param bKeepPrevious
* keep previous files or stop them to start a new one ?
* @param bPushNext
* whether the selection is added in first in queue
*/
private static void pushCommand(List<StackItem> alItems, boolean bKeepPrevious,
final boolean bPushNext) {
try {
// wake up FIFO if stopped
bStop = false;
// first try to mount needed devices
Iterator<StackItem> it = alItems.iterator();
boolean bNoMount = false;
while (it.hasNext()) {
StackItem item = it.next();
if (item == null) {
it.remove();
break;
}
// Do not synchronize this as we will wait for user response
if (!item.getFile().getDirectory().getDevice().isMounted()) {
if (!bNoMount) {
// not mounted, ok let them a chance to mount it:
final String sMessage = Messages.getString("Error.025") + " ("
+ item.getFile().getDevice().getName() + Messages.getString("FIFO.4");
int i = Messages.getChoice(sMessage, JOptionPane.YES_NO_CANCEL_OPTION,
JOptionPane.INFORMATION_MESSAGE);
if (i == JOptionPane.YES_OPTION) {
try {
item.getFile().getDevice().mount(true);
} catch (Exception e) {
it.remove();
Log.error(e);
Messages.showErrorMessage(11, item.getFile().getDevice().getName());
return;
}
} else if (i == JOptionPane.NO_OPTION) {
bNoMount = true; // do not ask again
// If only a single track was pushed and user decided not to
// mount its device, do not display another error message about void selection
if (alItems.size() == 1) {
return;
}
it.remove();
} else if (i == JOptionPane.CANCEL_OPTION) {
return;
}
} else {
it.remove();
}
}
}
synchronized (QueueModel.class) {
// test if we have yet some files to consider
if (alItems.size() == 0) {
Messages.showWarningMessage(Messages.getString("Warning.6"));
return;
}
// clear queue if selection contains some repeat items and we are not in repeat all mode
if (!Conf.getBoolean(Const.CONF_STATE_REPEAT_ALL)) {
for (StackItem item : alItems) {
if (item.isRepeat()) {
clear();
break;
}
}
}
// Reset repeat state for all previous items
setRepeatModeToAll(false);
// Position of insert into the queue
int pos = (queue.size() == 0) ? 0 : queue.size();
// OK, stop current track if no append
if (!bKeepPrevious && !bPushNext) {
index = pos;
Player.stop(false);
}
// if push to front, set pos to first item
else if (bPushNext) {
pos = index + 1;
}
// If push, not play, add items at the end
else if (bKeepPrevious && queue.size() > 0) {
pos = queue.size();
}
// add required tracks in the FIFO
for (StackItem item : alItems) {
if (pos >= queue.size()) {
queue.add(item);
} else {
queue.add(pos, item);
}
pos++;
JajukTimer.getInstance().addTrackTime(item.getFile());
}
// Apply repeat mode if required.
if (Conf.getBoolean(Const.CONF_STATE_REPEAT_ALL)) {
setRepeatModeToAll(true);
}
// launch track if required
if (!Player.isPlaying()) {
launch();
}
// computes planned tracks
computesPlanned(true);
}
} catch (Exception e) {
Log.error(e);
}
}
/**
* Contains repeated item.
*
* @param items
* The items to check for repeat.
*
* @return whether a stack item list contains a least one repeated item
*/
private static boolean containsRepeatedItem(List<StackItem> items) {
for (StackItem item : items) {
if (item.isRepeat()) {
return true;
}
}
return false;
}
/**
* Contains repeat.
*
* @return whether FIFO contains a least one repeated item
*/
public static boolean containsRepeat() {
return queue.containsRepeat();
}
/**
* Finished method, called by the PlayerImpl when the track is finished or
* should be finished (in case of intro mode or cross fade).
*/
public static void finished() {
finished(false);
}
/**
* Finished method, called by the PlayerImpl when the track is finished or
* should be finished (in case of intro mode, crass fade, previous/next
* track ...).
*
* @param forceNext
* whether to play the next track, even in single repeat.
*/
public static void finished(boolean forceNext) {
try {
// Tell jajuk not to enable fade-out for this kind of stop request
bInternalStop = true;
// If no playing item, just leave
StackItem current = getCurrentItem();
if (current == null) {
return;
}
notifyFinishedIfRequired();
computeNewIndex(forceNext, current);
unsetRepeatModeIfRequired(current);
// Leave if stop after current track option is set
if (bStopAfter) {
bStopAfter = false;
stopRequest();
return;
}
// Nothing more to play ? check if we are in continue mode
if (queue.size() == 0 || index >= queue.size()) {
if (Conf.getBoolean(Const.CONF_STATE_CONTINUE) && itemLast != null) {
final StackItem item = queue.popNextPlanned();
final File file;
// if some tracks are planned (can be 0 if planned size=0)
if (item != null) {
file = item.getFile();
} else {
// otherwise, take next track from file manager
file = FileManager.getInstance().getNextFile(itemLast.getFile());
}
if (file != null) {
// push it, it will be played
pushCommand(new StackItem(file), false, false);
} else {
// probably end of collection
endOfQueueReached();
}
} else {
endOfQueueReached();
return;
}
} else {
// something more in FIFO
launch();
}
// computes planned tracks
computesPlanned(false);
} catch (Exception e) {
Log.error(e);
} finally {
bInternalStop = false;
// refresh playlist editor
ObservationManager.notify(new JajukEvent(JajukEvents.QUEUE_NEED_REFRESH));
}
}
/**
* Drop the repeat properties of single tracks in single repeat mode to make
* sure already played tracks are no longer in repeat mode
* @param current
*/
private static void unsetRepeatModeIfRequired(StackItem current) {
if (Conf.getBoolean(Const.CONF_STATE_REPEAT) && current.isRepeat()) {
current.setRepeat(false);
}
}
/**
* Set a new value for the index. Note that this method has to be synchronized to handle concurrency from push
* @param forceNext
* @param current
*/
private static synchronized void computeNewIndex(boolean forceNext, StackItem current) {
if (Conf.getBoolean(Const.CONF_STATE_SHUFFLE) && queue.size() > 1
// In repeat mode, shuffle has no effect
&& !Conf.getBoolean(Const.CONF_STATE_REPEAT)) {
index = UtilSystem.getRandom().nextInt(queue.size() - 1);
} else if (current.isRepeat()) {
// if the track was in repeat mode, don't remove it from the
// fifo but increment the index
if (index < queue.size() - 1) {
StackItem itemNext = queue.get(index + 1);
// if next track is repeat, inc index
if (itemNext.isRepeat() || forceNext) {
index++;
}
} else { // We reached end of fifo
StackItem itemNext = queue.get(0);
// if next track is repeat, inc index, otherwise we keep the
// current index
if (itemNext.isRepeat() || forceNext) {
index = 0;
}
}
} else if (index < queue.size()) {
index++;
}
}
private static void notifyFinishedIfRequired() {
if (getPlayingFile() != null) {
Properties details = new Properties();
details.put(Const.DETAIL_CURRENT_FILE, getPlayingFile());
details.put(Const.DETAIL_CONTENT, Player.getActuallyPlayedTimeMillis());
ObservationManager.notify(new JajukEvent(JajukEvents.FILE_FINISHED, details));
}
}
/**
* @return the bInternalStop
*/
public static boolean isInternalStop() {
return bInternalStop || ExitService.isExiting();
}
/**
* To do when nothing more is to played,.
*/
private static void endOfQueueReached() {
stopRequest();
if (queue.size() > 0) {
ObservationManager.notify(new JajukEvent(JajukEvents.PLAYER_STOP));
} else {
ObservationManager.notify(new JajukEvent(JajukEvents.ZERO));
}
}
/**
* Launch track at 'index' position in the fifo.
*/
private static void launch() {
try {
// If no track playing at all, set first in queue
if (index < 0) {
index = 0;
}
UtilGUI.waiting();
File toPlay = getItem(index).getFile();
/**
* Force buttons to opening mode by default, then if they start
* correctly, a PLAYER_PLAY event will be notified to update to
* final state. We notify synchronously to make sure the order
* between these two events will be correct*
*/
ObservationManager.notifySync(new JajukEvent(JajukEvents.PLAY_OPENING));
// Check if we are in single repeat mode, transfer it to new
// launched track
if (Conf.getBoolean(Const.CONF_STATE_REPEAT)) {
getCurrentItem().setRepeat(true);
ObservationManager.notify(new JajukEvent(JajukEvents.QUEUE_NEED_REFRESH));
}
boolean bPlayOK = false;
// bfirstFile flag is used to set a offset (in %) if required (if we
// are playing the last item at given position)
// Known limitation : if the last session's last played item is no
// more available, the offset is applied
// to another file. We think that it doesn't worth making things
// more complicated.
if (bFirstFile && !Conf.getBoolean(Const.CONF_STATE_INTRO)
&& Conf.getString(Const.CONF_STARTUP_MODE).equals(Const.STARTUP_MODE_LAST_KEEP_POS)) {
// if it is the first played file of the session and we are in
// startup mode keep position
float fPos = UtilFeatures.readPersistedPlayingPosition();
// play it
bPlayOK = Player.play(toPlay, fPos, Const.TO_THE_END);
} else {
if (Conf.getBoolean(Const.CONF_STATE_INTRO)) {
// intro mode enabled
bPlayOK = Player.play(toPlay,
Float.parseFloat(Conf.getString(Const.CONF_OPTIONS_INTRO_BEGIN)) / 100,
1000 * Integer.parseInt(Conf.getString(Const.CONF_OPTIONS_INTRO_LENGTH)));
} else {
// normal mode
bPlayOK = Player.play(toPlay, 0.0f, Const.TO_THE_END);
}
}
if (bPlayOK) {
// notify to devices like commandJPanel to update UI when the
// play
// button has been pressed
ObservationManager.notify(new JajukEvent(JajukEvents.PLAYER_PLAY));
Log.debug("Now playing :" + toPlay);
// Send an event that a track has been launched
Properties pDetails = new Properties();
if (itemLast != null) {
pDetails.put(Const.DETAIL_OLD, itemLast);
}
pDetails.put(Const.DETAIL_CURRENT_FILE_ID, toPlay.getID());
pDetails.put(Const.DETAIL_CURRENT_DATE, Long.valueOf(System.currentTimeMillis()));
ObservationManager.notify(new JajukEvent(JajukEvents.FILE_LAUNCHED, pDetails));
// Save the last played track (even files in error are stored
// here as we need this for computes next track to launch after an
// error)
// We have to set this line here as we make directory change
// analyze before for cover change
itemLast = (StackItem) getCurrentItem().clone();
playingRadio = false;
bFirstFile = false;
// add hits number
toPlay.getTrack().incHits(); // inc hits number
// recalculate the total time left
JajukTimer.getInstance().reset();
for (int i = index; i < queue.size(); i++) {
JajukTimer.getInstance().addTrackTime(queue.get(i).getFile());
}
} else {
// Problem launching the track, try next one
UtilGUI.stopWaiting();
ObservationManager.notify(new JajukEvent(JajukEvents.QUEUE_NEED_REFRESH));
try {
Thread.sleep(Const.WAIT_AFTER_ERROR);
} catch (InterruptedException e) {
Log.error(e);
}
// save the last played track (even files in error are stored
// here as we need this for computes next track to launch after an
// error)
if (getCurrentItem() != null) {
itemLast = (StackItem) getCurrentItem().clone();
} else {
itemLast = null;
}
// We test if user required stop. Must be done here to make a
// chance to
// stop before starting a new track
if (!bStop) {
finished();
}
}
} catch (Throwable t) {// catch even Errors (OutOfMemory for example)
Log.error(122, t);
} finally {
UtilGUI.stopWaiting(); // stop the waiting cursor
}
}
/**
* Computes planned tracks.
*
* @param bClear
* : clear planned tracks stack
*/
public static void computesPlanned(boolean bClear) {
// Check if we are in continue mode and we have some tracks in FIFO, if
// not : no planned tracks
if (!Conf.getBoolean(Const.CONF_STATE_CONTINUE) || containsRepeat() || queue.size() == 0
|| Conf.getBoolean(Const.CONF_STATE_SHUFFLE)) {
queue.clearPlanned();
return;
}
if (bClear) {
queue.clearPlanned();
}
int missingPlannedSize = Conf.getInt(Const.CONF_OPTIONS_VISIBLE_PLANNED) - queue.sizePlanned();
for (int i = 0; i < missingPlannedSize; i++) {
StackItem item = null;
StackItem siLast = null; // last item in fifo or planned
// if planned stack contains yet some tracks
if (queue.sizePlanned() > 0) {
siLast = queue.getPlanned(queue.sizePlanned() - 1); // last
// one
} else if (queue.size() > 0) { // if fifo contains yet some
// tracks to play
siLast = queue.get(queue.size() - 1); // last one
}
try {
// if fifo contains yet some tracks to play
if (siLast != null) {
item = new StackItem(FileManager.getInstance().getNextFile(siLast.getFile()), false);
} else { // nothing in fifo, take first files in
// collection
List<File> files = FileManager.getInstance().getFiles();
item = new StackItem(files.get(0), false);
}
// Tell it is a planned item
item.setPlanned(true);
// add the new item
queue.addPlanned(item);
} catch (JajukException je) {
// can be thrown if FileManager return a null file (like
// when reaching the end of the collection)
break;
}
}
}
/**
* Contains only repeat.
*
* @return whether the FIFO contains only repeated files
*/
public static boolean containsOnlyRepeat() {
return queue.containsOnlyRepeat();
}
/**
* Play previous track.
*/
public static void playPrevious() {
try {
bStop = false;
// if playing, stop all playing players
if (Player.isPlaying()) {
Player.stop(true);
}
JajukTimer.getInstance().reset();
JajukTimer.getInstance().addTrackTime(queue);
// If we are playing first item, keep index = 0
if (index > 0) {
index--;
}
// Except if we are in full repeat, then we jump to last item
else if (Conf.getBoolean(Const.CONF_STATE_REPEAT_ALL)) {
index = queue.size() - 1;
}
launch();
} catch (Exception e) {
Log.error(e);
}
}
/**
* Play previous album.
*/
public static void playPreviousAlbum() {
try {
if (index <= 0) {
return;
}
bStop = false;
boolean bOK = false;
Directory dir = null;
if (getPlayingFile() != null) {
dir = getPlayingFile().getDirectory();
} else {// nothing in FIFO? just leave
return;
}
while (!bOK) {
// If we are playing first item, keep index = 0
if (index > 0) {
index--;
}
Directory dirTested = null;
File file = queue.get(index).getFile();
dirTested = file.getDirectory();
if (dir.equals(dirTested)) { // yet in the same album
continue;
} else {
// OK, previous is not in the same directory
// than current track, now check if it is the
// FIRST track from this new directory
if (FileManager.getInstance().isVeryfirstFile(file) ||
// this was the very first file from collection
(FileManager.getInstance().getPreviousFile(file) != null && FileManager.getInstance()
.getPreviousFile(file).getDirectory() != file.getDirectory())) {
// if true, it was the first track from the dir
bOK = true;
}
}
}
launch();
} catch (Exception e) {
Log.error(e);
}
}
/**
* Play next track in selection.
*/
public static void playNext() {
try {
bStop = false;
// if playing, stop current
if (Player.isPlaying()) {
Player.stop(true);
}
// force a finish to current track if any
if (getPlayingFile() != null) { // if stopped, nothing to stop
finished(true); // stop current track
} else if (itemLast != null) { // try to launch any previous
// file
pushCommand(itemLast, false, false);
} else { // really nothing? play a shuffle track from collection
File file = FileManager.getInstance().getShuffleFile();
if (file != null) {
pushCommand(new StackItem(file, Conf.getBoolean(Const.CONF_STATE_REPEAT_ALL), false),
false, false);
}
}
} catch (Exception e) {
Log.error(e);
}
}
/**
* Play next track in selection.
*/
public static void playNextAlbum() {
try {
bStop = false;
// if playing, stop all playing players
if (Player.isPlaying()) {
Player.stop(true);
}
// we don't support album navigation inside repeated tracks
if (getQueueSize() > 0 && getItem(0).isRepeat()) {
playNext();
return;
}
int indexFirstItem = -1;
if (getPlayingFile() != null) {
// ref directory
Directory dir = getPlayingFile().getDirectory();
// scan current fifo and try to launch the first track not from
// this album
for (int i = getIndex(); i < queue.size(); i++) {
File file = getItem(i).getFile();
if (!file.getDirectory().equals(dir)) {
indexFirstItem = i;
break;
}
}
}
if (indexFirstItem > 0) {
// some tracks of other album were already in
// fifo
// add a fake album at the top the fifo because the
// finish will drop first element and we won't
// drop first track of the next album
goTo(indexFirstItem);
} else if (itemLast != null) {// void fifo, add next album
File fileNext = itemLast.getFile();
fileNext = FileManager.getInstance().getNextAlbumFile(fileNext);
// Now add the associated album to the
Album album = fileNext.getTrack().getAlbum();
List<File> files = UtilFeatures.getPlayableFiles(album);
List<StackItem> stack = UtilFeatures.createStackItems(UtilFeatures.applyPlayOption(files),
Conf.getBoolean(Const.CONF_STATE_REPEAT_ALL), true);
// Find index to go to (first index with a file whose dir is
// different
// from current one)
int index = getIndex();
Directory currentDir = null;
if (index < queue.size()) {
currentDir = queue.get(index).getFile().getDirectory();
}
while (index < queue.size() && queue.get(index).getFile().getDirectory().equals(currentDir)) {
index++;
}
queue.addAll(index, stack);
goTo(index);
}
} catch (Exception e) {
Log.error(e);
}
}
/**
* Get the currently played file or null if no playing file.
*
* @return File
*/
public static File getPlayingFile() {
if (isStopped()) {
return null;
}
StackItem item = getCurrentItem();
return (item == null) ? null : item.getFile();
}
/**
* Get the currently played file or null if no playing file.
*
* @return File
*/
public static String getPlayingFileTitle() {
File file = getPlayingFile();
if (file != null) {
String pattern = Conf.getString(Const.CONF_PATTERN_FRAME_TITLE);
String title = null;
try {
title = UtilString.applyPattern(file, pattern, false, false);
} catch (JajukException e) {
Log.error(e);
}
return title;
}
return null;
}
/**
* Get the currently played stack item or null if no playing item.
*
* @return stack item
*/
public static StackItem getCurrentItem() {
if (index < queue.size() && index >= 0) {
return queue.get(index);
} else {
return null;
}
}
/**
* Get an item at given index in FIFO.
*
* @param index
* : index
*
* @return stack item
*/
public static StackItem getItem(int index) {
return queue.get(index);
}
/**
* Get index of the last repeated item, -1 if none repeated.
*
* @return index
*/
private static int getLastRepeatedItem() {
int i = -1;
Iterator<StackItem> iterator = queue.iterator();
while (iterator.hasNext()) {
StackItem item = iterator.next();
if (item.isRepeat()) {
i++;
} else {
break;
}
}
return i;
}
/**
* Return true if none file is playing or planned to play for the given
* device.
*
* @param device
* device to unmount
*
* @return true, if can unmount
*/
public static boolean canUnmount(Device device) {
if (isStopped() || isPlayingRadio()) {
return true;
}
if (getPlayingFile() != null && getPlayingFile().getDirectory().getDevice().equals(device)) {
// is current track on this device?
return false;
}
// work on a queue copy to avoid the concurrent access issues
List<StackItem> copy = new ArrayList<StackItem>(queue);
Iterator<StackItem> it = copy.iterator();
// are next tracks in fifo on this device?
while (it.hasNext()) {
StackItem item = it.next();
File file = item.getFile();
if (file.getDirectory().getDevice().equals(device)) {
queue.remove(item);
}
}
return true;
}
/**
* Stop request.
*/
public static void stopRequest() {
// fifo is over ( stop request ) , reinit labels in information panel
// before exiting
bStop = true;
Player.stop(true); // stop player
// No more playing webradio
playingRadio = false;
// notify views like commandJPanel to update ui
ObservationManager.notify(new JajukEvent(JajukEvents.PLAYER_STOP));
}
/**
* Returns whether FIFO is stopped or not <br>
* Caution ! the FIFO may be stopped but current track is not void and a web
* radio can be playing.
*
* @return Returns whether FIFO is stopped or not
*/
public static boolean isStopped() {
return bStop;
}
/**
* Gets the queue.
*
* @return Returns a defensive copy of the fifo
*/
public static List<StackItem> getQueue() {
return queue.getQueue();
}
/**
* Return queue size.
*
* @return FIFO size (do not use getFIFO().size() for performance reasons)
*/
public static int getQueueSize() {
return queue.size();
}
/**
* Insert a file to play in FIFO at specified position.
*
* @param item
* the item to insert.
* @param iPos
* The position where the item is inserted.
*/
public static void insert(StackItem item, int iPos) {
List<StackItem> alStack = new ArrayList<StackItem>(1);
alStack.add(item);
insert(alStack, iPos);
}
/**
* Insert a file at specified position, any existing item at this position
* is shifted on the right.
*
* @param alFiles
* The list of items to insert.
* @param iPos
* The position where the items are inserted.
*/
public static void insert(List<StackItem> alFiles, int iPos) {
if (iPos <= queue.size()) {
// add in the FIFO, accept a file at
// size() position to allow increasing
// FIFO at the end
queue.addAll(iPos, alFiles);
if (iPos <= index) {
index += alFiles.size();
}
JajukTimer.getInstance().addTrackTime(alFiles);
}
computesPlanned(false);
// refresh queue
ObservationManager.notify(new JajukEvent(JajukEvents.QUEUE_NEED_REFRESH));
}
/**
* Put up an item from given index to index-1.
*
* @param lIndex
* The index to move up in the queue.
*/
public static void up(int lIndex) {
if (lIndex == 0 || lIndex >= queue.size()) {
// Can't put up first track in queue or
// first planned track.
// This should be already made by ui behavior
return;
}
if (lIndex < queue.size()) {
StackItem item = queue.get(lIndex);
queue.remove(lIndex); // remove the item
queue.add(lIndex - 1, item); // add it again above
if (lIndex == index) {
index--;
}
}
}
/**
* Put down an item from given index to index+1.
*
* @param lIndex
* The index to move down in the queue.
*/
public static void down(int lIndex) {
if (lIndex >= queue.size() - 1) {
// Can't put down last track in FIFO. This should be already made by
// ui behavior
return;
}
StackItem item = queue.get(lIndex);
queue.remove(lIndex); // remove the item
queue.add(lIndex + 1, item); // add it again above
if (lIndex == index) {
index++;
}
}
/**
* Go to given index and launch it.
*
* @param pIndex
* The index to go to in the queue.
*/
public static void goTo(final int pIndex) {
bStop = false;
try {
index = pIndex;
// need to stop before launching! this fixes a
// wrong EOM event in BasicPlayer
Player.stop(false);
launch();
} catch (Exception e) {
Log.error(e);
}
}
/**
* Remove a track at specified index from the queue model.
*
* @param int index
* index of the item to remove
*
**/
public static void remove(final int index) {
Set<Integer> indexes = new HashSet<Integer>(1);
indexes.add(index);
remove(indexes);
}
/**
* Remove files at specified indexes.
* @param Set<Integer> indexes
* set of index to drop. We expect the array to contain integers sorted by ascendent order. The set may be void (a warning is then logged) but not null
*/
public static void remove(final Set<Integer> initialIndexes) {
List<Integer> indexes = new ArrayList<Integer>(initialIndexes);
// controls indexes
if (indexes.size() == 0) {
Log.warn("Removal required for a void list of indexes");
return;
}
for (int indexToRemove : indexes) {
if (indexToRemove < 0 || indexToRemove >= queue.size()) {
throw new IllegalStateException("Illegal removal index : " + index + " / " + queue.size()
+ " / " + queue.sizePlanned());
}
}
boolean removePlayedTrack = isPlayingTrack() && initialIndexes.contains(QueueModel.index);
boolean removePlayedTrackThatIsLastInQueue = removePlayedTrack
&& indexes.get(indexes.size() - 1) == (queue.size() - 1);
StackItem firstPlannedTrack = null;
List<StackItem> plannedQueue = QueueModel.getPlanned();
if (plannedQueue.size() > 0) {
firstPlannedTrack = plannedQueue.get(0);
firstPlannedTrack.setPlanned(false);
}
for (int indexToRemove : indexes) {
// Remove this file from fifo and recompute current index if required.
// We have to decrement current index if we drop tracks prior current or
// if we dropped the last item.
queue.remove(indexToRemove);
if (indexToRemove < index || removePlayedTrackThatIsLastInQueue) {
index--;
}
// Recompute indexes to remove to take into account the offset created by the removal.
// First indexes may become negative, this is not an issue as they are already processed.
int comp = 0;
for (int i : indexes) {
if (i >= indexToRemove) {
indexes.set(comp, i - 1);
}
comp++;
}
}
// Take launcher actions due to removals
// If this is the playing track, stop it before dropping it
// However, we have an issue if we remove the last file of the queue if it is playing : the new first
// planned track of the new queue is the removed file itself so we would replay the removed track again.
// To avoid this, we store the first planned track and we force it.
if (removePlayedTrack) {
if (removePlayedTrackThatIsLastInQueue) {
if (firstPlannedTrack != null) { // null is not in continue mode
push(firstPlannedTrack, false);
} else {
stopRequest();
}
} else {
goTo(index);
}
}
// Recomputes all planned tracks from last file in fifo
computesPlanned(true);
}
/**
* Gets the last.
*
* @return Last Stack item in FIFO
*/
public static StackItem getLast() {
if (queue.size() == 0) {
return null;
}
return queue.get(queue.size() - 1);
}
/**
* Gets the last played.
*
* @return Last played item
*/
public static StackItem getLastPlayed() {
return itemLast;
}
/**
* Gets the index.
*
* @return Returns the index.
*/
public static int getIndex() {
return index;
}
/**
* Gets the count tracks left.
*
* @return the count tracks left
*/
public static int getCountTracksLeft() {
if (index == -1) {
// none playing track
return queue.size();
}
return queue.size() - index;
}
/**
* Gets the planned.
*
* @return Returns a defensive copy of planned files
*/
public static List<StackItem> getPlanned() {
return queue.getPlanned();
}
/**
* Store current FIFO as a list.
*
* @throws IOException
* Signals that an I/O exception has occurred.
*/
public static void commit() throws IOException {
java.io.File out = SessionService.getConfFileByPath(Const.FILE_FIFO + "."
+ Const.FILE_SAVING_FILE_EXTENSION);
PrintWriter writer = new PrintWriter(new BufferedOutputStream(new FileOutputStream(out, false)));
for (StackItem st : queue) {
writer.println(st.getFile().getID());
}
writer.flush();
writer.close();
//Store index
Conf.setProperty(Const.CONF_STARTUP_QUEUE_INDEX, Integer.toString(index));
// Override initial file
java.io.File finalFile = SessionService.getConfFileByPath(Const.FILE_FIFO);
UtilSystem.saveFileWithRecoverySupport(finalFile);
Log.debug("Queue commited to : " + finalFile.getAbsolutePath());
}
/**
* Return whether a web radio is being played.
*
* @return whether a web radio is being played
*/
public static boolean isPlayingRadio() {
return playingRadio;
}
/**
* Return current web radio if any or null otherwise.
*
* @return current web radio if any or null otherwise
*/
public static WebRadio getCurrentRadio() {
return currentRadio;
}
/**
* Return whether a track is being played.
*
* @return whether a track is being played
*/
public static boolean isPlayingTrack() {
return !bStop && !isPlayingRadio();
}
/**
* Gets the current file title.
*
* @return a string representation for current played item or stop state
*/
public static String getCurrentFileTitle() {
String title = null;
File file = getPlayingFile();
if (isPlayingRadio()) {
Properties webradioInfoUpdatedEvent = ObservationManager
.getDetailsLastOccurence(JajukEvents.WEBRADIO_INFO_UPDATED);
// TODO Strange but we experienced NPE here coming from a call from tray/mouseMoved, so we perform sanity check, to be investigated
if (webradioInfoUpdatedEvent != null) {
WebRadio updatedWebRadio = (WebRadio) webradioInfoUpdatedEvent.get(Const.DETAIL_CONTENT);
if (getCurrentRadio().getName().equals(updatedWebRadio.getName())) {
title = (String) webradioInfoUpdatedEvent.get(Const.CURRENT_RADIO_TRACK);
} else {
title = getCurrentRadio().getName();
}
} else {
title = Messages.getString("JajukWindow.18");
}
} else if (file != null && !isStopped()) {
title = file.getHTMLFormatText();
} else {
title = Messages.getString("JajukWindow.18");
}
return title;
}
/**
* Force FIFO cleanup, for example after files deletion.
*/
public static synchronized void clean() {
Iterator<StackItem> it = queue.iterator();
int i = 0;
while (it.hasNext()) {
StackItem si = it.next();
if (FileManager.getInstance().getFileByID(si.getFile().getID()) == null) {
it.remove();
if (i <= index) {
index--;
}
}
i++;
}
computesPlanned(true);
}
/**
* Force FIFO index.
*
* @param index
*
*
* @pram index index to set
*/
public static synchronized void setIndex(int index) {
QueueModel.index = index;
}
/**
* Sets the stop after.
*
* @param stopAfter Whether we should stop after current track playback
*/
public static void setStopAfter(boolean stopAfter) {
QueueModel.bStopAfter = stopAfter;
}
}