/**
* Xtreme Media Player a cross-platform media player.
* Copyright (C) 2005-2011 Besmir Beqiri
*
* 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 (at your option) 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package xtrememp.playlist;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import xtrememp.playlist.filter.Predicate;
import xtrememp.playlist.filter.TruePredicate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Playlist implementation.
*
* @author Besmir Beqiri
*/
public class Playlist {
private static Logger logger = LoggerFactory.getLogger(Playlist.class);
public enum PlayMode {
REPEAT_NONE,
REPEAT_ONE,
REPEAT_ALL,
SHUFFLE
}
protected final List<PlaylistItem> cachedPlaylist;
protected final List<PlaylistItem> filteredPlaylist;
protected final List<PlaylistItem> shuffledList;
protected final List<PlaylistListener> listeners;
protected final Random rnd;
protected Predicate<PlaylistItem> filterPredicate;
protected PlayMode playMode = PlayMode.REPEAT_ALL;
protected PlaylistItem cursor;
protected int shuffledIndex = -1;
protected boolean isModified = false;
/**
* Default constructor.
*/
public Playlist() {
cachedPlaylist = new ArrayList<PlaylistItem>();
filteredPlaylist = new ArrayList<PlaylistItem>();
shuffledList = new ArrayList<PlaylistItem>();
listeners = new ArrayList<PlaylistListener>();
rnd = new Random();
filterPredicate = TruePredicate.<PlaylistItem>getInstance();
}
/**
* Return the current play mode.
*
* @return A {@link PlayMode} object.
*/
public PlayMode getPlayMode() {
return playMode;
}
/**
* Set the play mode for the playlist. It changes the behaviour of the
* playlist on determining the next or previous item to be played or not.
*
* @param playMode The {@link PlayMode} to set.
*/
public void setPlayMode(PlayMode playMode) {
this.playMode = playMode;
if (playMode == PlayMode.SHUFFLE) {
int size = size();
switch (size) {
case 1:
shuffledList.add(filteredPlaylist.get(0));
break;
default:
for (int i = 0; i < size; i++) {
shuffledList.add(rnd.nextInt(shuffledList.size() + 1),
filteredPlaylist.get(i));
}
int cursorShuffledIndex = shuffledList.indexOf(cursor);
if (cursorShuffledIndex != -1) {
Collections.swap(shuffledList, 0, cursorShuffledIndex);
}
break;
}
shuffledIndex = 0;
} else {
shuffledList.clear();
shuffledIndex = -1;
}
firePlayModeChangedEvent();
}
/**
* Appends a playlist item at the end of the playlist.
*
* @param item A playlist item.
* @return <code>true</code> if item was successfully added, else <code>false</code>.
*/
public boolean addItem(PlaylistItem item) {
boolean added = cachedPlaylist.add(item);
if (filterPredicate.evaluate(item)) {
filteredPlaylist.add(item);
}
setModified(added);
if (playMode == PlayMode.SHUFFLE) {
addToShuffledList(item);
}
fireItemAddedEvent(item);
return added;
}
/**
* Adds a playlist item at a given position in the playlist.
*
* @param pos The position of the item.
* @param item A playlist item.
*/
public void addItemAt(int pos, PlaylistItem item) {
cachedPlaylist.add(pos, item);
if (filterPredicate.evaluate(item)) {
filteredPlaylist.add(pos, item);
}
setModified((item == null) ? false : true);
if (playMode == PlayMode.SHUFFLE) {
addToShuffledList(item);
}
fireItemAddedEvent(item);
}
/**
* Adds a collection of items to the playlist.
*
* @param c A collection of items.
* @return <code>true</code> if the collection was successfully added,
* else <code>false</code>.
*/
public boolean addAll(Collection<? extends PlaylistItem> c) {
boolean added = cachedPlaylist.addAll(c);
for (PlaylistItem item : c) {
if (filterPredicate.evaluate(item)) {
filteredPlaylist.add(item);
}
}
setModified(added);
if (playMode == PlayMode.SHUFFLE) {
for (PlaylistItem item : c) {
addToShuffledList(item);
}
}
for (PlaylistItem item : c) {
fireItemAddedEvent(item);
}
return added;
}
/**
* Removes the specified item from the playlist.
*
* @param item A playlist item.
* @return <code>true</code> if item was successfully removed, else <code>false</code>.
*/
public boolean removeItem(PlaylistItem item) {
boolean removed = cachedPlaylist.remove(item);
filteredPlaylist.remove(item);
setModified(removed);
if (playMode == PlayMode.SHUFFLE) {
removeFromShuffledList(item);
}
fireItemRemovedEvent(item);
return removed;
}
/**
* Removes a playlist item at a given position from the playlist.
*
* @param pos The position of the item.
* @return The playlist item that was removed.
*/
public PlaylistItem removeItemAt(int pos) {
PlaylistItem item = cachedPlaylist.remove(pos);
filteredPlaylist.remove(item);
setModified((item == null) ? false : true);
if (playMode == PlayMode.SHUFFLE) {
removeFromShuffledList(item);
}
fireItemRemovedEvent(item);
return item;
}
/**
* Removes a collection of items from the playlist.
*
* @param c A collection of items.
* @return <code>true</code> if this playlist changed as a result of the call.
*/
public boolean removeAll(Collection<? extends PlaylistItem> c) {
boolean removed = cachedPlaylist.removeAll(c);
filteredPlaylist.removeAll(c);
setModified(removed);
if (playMode == PlayMode.SHUFFLE) {
for (PlaylistItem item : c) {
removeFromShuffledList(item);
}
}
for (PlaylistItem item : c) {
fireItemRemovedEvent(item);
}
return removed;
}
/**
* Removes all items from the playlist.
*/
public void clear() {
Iterator<PlaylistItem> iterator = cachedPlaylist.iterator();
while (iterator.hasNext()) {
PlaylistItem item = iterator.next();
iterator.remove();
fireItemRemovedEvent(item);
}
filteredPlaylist.clear();
shuffledList.clear();
shuffledIndex = 0;
begin();
}
/**
* Sorts the entire playlist based on the given comparator.
*
* @param comparator A {@link Comparator} object.
*/
public void sort(Comparator<PlaylistItem> comparator) {
Collections.sort(cachedPlaylist, comparator);
Collections.sort(filteredPlaylist, comparator);
setModified(true);
}
/**
* Filters the entire playlist based on the given predicate.
*
* @param filterPredicate A {@link Predicate} object.
*/
public void filter(Predicate<PlaylistItem> filterPredicate) {
this.filterPredicate = filterPredicate;
filteredPlaylist.clear();
if (playMode == PlayMode.SHUFFLE) {
shuffledList.clear();
shuffledIndex = 0;
}
for (PlaylistItem pli : cachedPlaylist) {
if (filterPredicate.evaluate(pli)) {
filteredPlaylist.add(pli);
if (playMode == PlayMode.SHUFFLE) {
addToShuffledList(pli);
}
}
}
setModified(true);
}
/**
* Returns <code>true</code> if the playlist is filtered.
*/
public boolean isFiltered() {
return filterPredicate != TruePredicate.<PlaylistItem>getInstance();
}
/**
* Moves a playlist item to a new position.
*
* @param fromPos The current position.
* @param toPos The new position.
*/
public void moveItem(int fromPos, int toPos) {
int newIndex = cachedPlaylist.indexOf(filteredPlaylist.get(toPos));
PlaylistItem pli = filteredPlaylist.remove(fromPos);
if (pli != null) {
filteredPlaylist.add(toPos, pli);
if (cachedPlaylist.remove(pli)) {
cachedPlaylist.add(newIndex, pli);
}
}
setModified(true);
}
/**
* Shuffles items in the playlist randomly.
*/
public void randomize() {
Collections.shuffle(cachedPlaylist);
Collections.shuffle(filteredPlaylist);
setModified(true);
}
/**
* Reverses the order of the items in the playlist.
*/
public void reverse() {
Collections.reverse(cachedPlaylist);
Collections.reverse(filteredPlaylist);
setModified(true);
}
/**
* Moves the cursor at the begining of the Playlist.
*/
public void begin() {
cursor = null;
if (!filteredPlaylist.isEmpty()) {
if (playMode == PlayMode.SHUFFLE) {
cursor = getShuffledCursor(true);
} else {
cursor = filteredPlaylist.get(0);
}
}
setModified(true);
}
/**
* Returns the playlist item at a given position from the playlist.
*
* @param pos The position of the item.
* @return A playlist item.
*/
public PlaylistItem getItemAt(int pos) {
return filteredPlaylist.get(pos);
}
/**
* Returns a list of all playlist items the playlist contains.
*
* @return A list of playlist items.
*/
public List<PlaylistItem> listAllItems() {
return cachedPlaylist;
}
/**
* Returns a filtered list of all playlist items based on the applied filter.
*
* @return A list of playlist items.
*/
public List<PlaylistItem> listItems() {
return filteredPlaylist;
}
/**
* Returns the number of items (size) in the playlist.
*
* @return An integer value.
*/
public int size() {
return filteredPlaylist.size();
}
/**
* Computes cursor position (next).
*/
public void nextCursor() {
if (playMode == PlayMode.SHUFFLE) {
cursor = getShuffledCursor(true);
} else {
int cursorPos = getCursorPosition();
cursorPos++;
if (cursorPos > size() - 1) {
cursorPos = 0;
}
cursor = getItemAt(cursorPos);
}
}
/**
* Computes cursor position (previous).
*/
public void previousCursor() {
if (playMode == PlayMode.SHUFFLE) {
cursor = getShuffledCursor(false);
} else {
int cursorPos = getCursorPosition();
cursorPos--;
if (cursorPos < 0) {
cursorPos = size() - 1;
}
cursor = getItemAt(cursorPos);
}
}
/**
* Set the modification flag for the playlist.
*
* @param flag if <code>true</code>, sets the modification flag as modified,
* if <code>false</code> as not modified.
*/
public void setModified(boolean flag) {
isModified = flag;
}
/**
* Returns the playlist item matching to the cursor.
*
* @return A playlist item.
*/
public PlaylistItem getCursor() {
return cursor;
}
/**
* Replaces the cursor with a new playlist item.
*
* @param newCursor A playlist item.
*/
public void setCursor(PlaylistItem newCursor) {
cursor = newCursor;
if ((playMode == PlayMode.SHUFFLE) && (cursor != null)) {
if (shuffledList.size() > 1
&& shuffledList.get(shuffledIndex) != cursor) {
shuffledIndex = (++shuffledIndex > shuffledList.size() - 1) ? 0 : shuffledIndex;
Collections.swap(shuffledList, shuffledIndex,
shuffledList.indexOf(cursor));
}
}
}
/**
* Returns the position matching to the cursor.
*
* @return An integer value.
*/
public int getCursorPosition() {
return indexOf(cursor);
}
/**
* Replaces the cursor with a new playlist item specified by its position.
*
* @param pos An integer value.
*/
public void setCursorPosition(int pos) {
cursor = getItemAt(pos);
}
/**
* Adds the specified playlist item to the shuffled list in a random position.
*
* @param item A playlist item.
*/
private void addToShuffledList(PlaylistItem item) {
int randomIndex = shuffledIndex + rnd.nextInt(shuffledList.size() - shuffledIndex + 1);
shuffledList.add(randomIndex, item);
}
/**
* Removes the first occurrence of the specified playlist item from the
* shuffled list, if it is present.
*
* @param item A playlist item.
*/
private void removeFromShuffledList(PlaylistItem item) {
shuffledList.remove(item);
if (shuffledList.isEmpty()) {
shuffledIndex = 0;
} else {
int maxIndex = shuffledList.size() - 1;
shuffledIndex = (shuffledIndex > maxIndex) ? maxIndex : shuffledIndex;
}
}
/**
* Returns a randomly generated cursor position. This method makes sure that
* all playlist items of this playlist will be selected once before
* selecting the same item twice.
*
* @param next If <code>true</code> retrive the next cursor position value,
* else the previous one.
* @return An integer value between [0, playlist size - 1], otherwise -1 if
* this playlist is empty.
*/
private PlaylistItem getShuffledCursor(boolean next) {
if (!shuffledList.isEmpty()) {
shuffledIndex = (next) ? ((++shuffledIndex > shuffledList.size() - 1)
? 0 : shuffledIndex) : ((--shuffledIndex < 0)
? shuffledList.size() - 1 : shuffledIndex);
return shuffledList.get(shuffledIndex);
} else {
return null;
}
}
/**
* Returns the index of the specified playlist item.
*
* @param item A playlist item.
* @return An integer value.
*/
public int indexOf(PlaylistItem item) {
return filteredPlaylist.indexOf(item);
}
/**
* Checks the modification flag.
*
* @return <code>true</code> if the playlist is modified, else <code>false</code>.
*/
public boolean isModified() {
return isModified;
}
/**
* Checks if the playlist is empty.
*
* @return <code>true</code> if the playlist is empty, else <code>false</code>.
*/
public boolean isEmpty() {
return filteredPlaylist.isEmpty();
}
/**
* Adds the specified playlist listener to receive playlist events from this
* playlist. If the listener is <code>null</code>, no exception is thrown
* and no action is performed.
*
* @param listener A playlist listener.
*/
public void addPlaylistListener(PlaylistListener listener) {
if (listener == null) {
return;
}
listeners.add(listener);
logger.info("Playlist listener added");
}
/**
* Removes the specified playlist listener so that it no longer receives
* playlist events from this playlist. This method performs no function,
* nor does it throw an exception, if the listener specified by the argument
* was not previously added to this playlist.
* If the listener is <code>null</code>, no exception is thrown and no
* action is performed.
*
* @param listener A playlist listener.
*/
public void removePlaylistListener(PlaylistListener listener) {
if (listener == null) {
return;
}
listeners.remove(listener);
logger.info("Playlist listener removed");
}
/**
* Notifies all listeners that a playlist item has been added.
*
* @param item The playlist item added.
*/
private void fireItemAddedEvent(PlaylistItem item) {
PlaylistEvent event = new PlaylistEvent(this, item);
for (PlaylistListener listener : listeners) {
listener.playlistItemAdded(event);
}
logger.info("Playlist item added: {}", item);
}
/**
* Notifies all listeners that a playlist item has been removed.
*
* @param item The playlist item removed.
*/
private void fireItemRemovedEvent(PlaylistItem item) {
PlaylistEvent event = new PlaylistEvent(this, item);
for (PlaylistListener listener : listeners) {
listener.playlistItemRemoved(event);
}
logger.info("Playlist item removed: {}", item);
}
/**
* Notifies all listeners that the play mode has changed.
*/
private void firePlayModeChangedEvent() {
PlaylistEvent event = new PlaylistEvent(this);
for (PlaylistListener listener : listeners) {
listener.playModeChanged(event);
}
logger.info("Play mode changed: {}", playMode);
}
}