/*
* Copyright (c) 2008, 2009, 2010, 2011 Denis Tulskiy
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* version 3 along with this work. If not, see <http://www.gnu.org/licenses/>.
*/
package com.tulskiy.musique.playlist;
import com.tulskiy.musique.playlist.formatting.Parser;
import com.tulskiy.musique.playlist.formatting.tokens.Expression;
import com.tulskiy.musique.system.Application;
import com.tulskiy.musique.system.configuration.Configuration;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.List;
/**
* Manages playback.
* <p/>
* Idea fot the shuffle algorithm taken from streamer.c from DeadBeef project
* <p/>
* Author: Denis Tulskiy
* Date: Jul 1, 2010
*/
public class PlaybackOrder {
public enum Order {
DEFAULT("Default"),
REPEAT("Repeat"),
REPEAT_TRACK("Repeat track"),
REPEAT_ALBUM("Repeat album"),
REPEAT_GROUP("Repeat group"),
SHUFFLE("Shuffle"),
SHUFFLE_ALBUMS("Shuffle albums"),
SHUFFLE_GROUPS("Shuffle groups"),
RANDOM("Random");
private String text;
Order(String text) {
this.text = text;
}
@Override
public String toString() {
return text;
}
}
private static Expression queueTupleTitle = Parser.parse("[%artist% - ]%title%");
public class QueueTuple {
public Track track;
public Playlist playlist;
QueueTuple(Track track, Playlist playlist) {
this.track = track;
this.playlist = playlist;
}
@Override
public String toString() {
return (String) queueTupleTitle.eval(track);
}
}
private Playlist playlist;
private Order order = Order.DEFAULT;
private List<QueueTuple> queue = new ArrayList<QueueTuple>();
private Track lastPlayed;
private Expression albumFormat;
public PlaybackOrder() {
final Configuration config = Application.getInstance().getConfiguration();
config.addPropertyChangeListener("playbackOrder.albumFormat", true, new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
String format = config.getString(evt.getPropertyName(), "%album%");
albumFormat = Parser.parse(format);
}
});
}
public void setPlaylist(Playlist playlist) {
this.playlist = playlist;
}
public void setOrder(Order order) {
this.order = order;
}
public void setLastPlayed(Track lastPlayed) {
this.lastPlayed = lastPlayed;
}
public Track getLastPlayed() {
return lastPlayed;
}
public List<QueueTuple> getQueue() {
return queue;
}
public void enqueue(Track track, Playlist playlist) {
queue.add(new QueueTuple(track, playlist));
updateQueuePositions();
}
public void updateQueuePositions() {
for (int i = 0; i < queue.size(); i++) {
queue.get(i).track.setQueuePosition(i + 1);
}
}
public void flushQueue() {
for (QueueTuple tuple : queue) {
tuple.track.setQueuePosition(-1);
}
queue.clear();
updateQueuePositions();
}
private Track getTrack(int index) {
if (index != -1) {
Track track = playlist.get(index);
// technically, separator can not be the last track
// so we just get the next track
if (track.getTrackData().getLocation() == null)
return playlist.get(index + 1);
return track;
} else {
return null;
}
}
private Track next(int index) {
index = index < playlist.size() - 1 ? index + 1 : -1;
if (index != -1) {
Track track = playlist.get(index);
// technically, separator can not be the last track
// so we just get the next track
if (track.getTrackData().getLocation() == null)
return next(index);
return track;
} else {
return null;
}
}
private Track prev(int index) {
index--;
if (index >= 0) {
Track track = playlist.get(index);
if (track.getTrackData().getLocation() == null)
return prev(index);
return track;
} else {
return null;
}
}
public Track next(Track currentTrack) {
int index;
if (!queue.isEmpty()) {
QueueTuple tuple = queue.remove(0);
Track track = tuple.track;
setPlaylist(tuple.playlist);
track.setQueuePosition(-1);
updateQueuePositions();
return track;
}
if (playlist == null || playlist.size() <= 0)
return null;
if (lastPlayed != null) {
if (playlist.contains(lastPlayed)) {
Track track = lastPlayed;
lastPlayed = null;
return track;
}
}
if (currentTrack == null) {
return playlist.get(0);
} else {
index = playlist.indexOf(currentTrack);
if (index == -1)
return playlist.get(0);
Track track;
switch (order) {
case DEFAULT:
return next(index);
case REPEAT:
track = next(index);
return track != null ? track : getTrack(0);
case REPEAT_TRACK:
return currentTrack;
case REPEAT_ALBUM:
return nextPatternMatch(currentTrack, index, albumFormat, false);
case REPEAT_GROUP:
if (index + 1 < playlist.size()) {
Track tr = playlist.get(index + 1);
if (tr.getTrackData().getLocation() != null) {
return tr;
}
}
for (int i = index; i >= 0; i--) {
if (playlist.get(i).getTrackData().getLocation() == null) {
return playlist.get(i + 1);
}
}
return playlist.get(0);
case SHUFFLE_ALBUMS:
return nextPatternMatch(currentTrack, index, albumFormat, true);
case SHUFFLE_GROUPS:
if (index + 1 < playlist.size()) {
Track tr = playlist.get(index + 1);
if (tr.getTrackData().getLocation() != null) {
return tr;
}
}
for (int i = index; i >= 0; i--) {
Track separator = playlist.get(i);
if (separator.getTrackData().getLocation() == null) {
separator = nextShuffle(separator, true, null);
return next(playlist.indexOf(separator));
}
}
return playlist.get(0);
case RANDOM:
return nextRandom();
case SHUFFLE:
return nextShuffle(currentTrack, false, null);
}
}
return getTrack(index);
}
private Track nextPatternMatch(Track currentTrack, int index, Expression pattern, boolean shuffle) {
Track track;
Object result = pattern.eval(currentTrack);
track = next(index);
if (track != null) {
if (equals(result, pattern.eval(track))) {
return track;
}
}
for (int i = index; i >= 0; i--) {
track = playlist.get(i);
if (!equals(result, pattern.eval(track))) {
Track next = next(i);
if (shuffle) {
return nextShuffle(next, false, pattern);
} else {
return next;
}
}
}
return track;
}
public Track prev(Track currentTrack) {
if (playlist == null || playlist.size() <= 0)
return null;
int index = playlist.indexOf(currentTrack);
if (index == -1)
return null;
int size = playlist.size();
Track track;
switch (order) {
case DEFAULT:
return prev(index);
case REPEAT:
track = prev(index);
return track != null ? track : getTrack(size - 1);
case REPEAT_TRACK:
return currentTrack;
case REPEAT_ALBUM:
return prevPatternMatch(currentTrack, index, albumFormat, false);
case REPEAT_GROUP:
if (index > 0) {
Track tr = playlist.get(index - 1);
if (tr.getTrackData().getLocation() != null) {
return tr;
}
}
for (int i = index + 1; i < size; i++) {
if (playlist.get(i).getTrackData().getLocation() == null) {
return playlist.get(i - 1);
}
}
return playlist.get(size - 1);
case SHUFFLE_ALBUMS:
return prevPatternMatch(currentTrack, index, albumFormat, true);
case SHUFFLE_GROUPS:
if (index > 0) {
Track tr = playlist.get(index - 1);
if (tr.getTrackData().getLocation() != null) {
return tr;
}
}
for (int i = index; i >= 0; i--) {
Track separator = playlist.get(i);
if (separator.getTrackData().getLocation() == null) {
separator = prevShuffle(separator, true, null);
return next(playlist.indexOf(separator));
}
}
return playlist.get(0);
case RANDOM:
return nextRandom();
case SHUFFLE:
return prevShuffle(currentTrack, false, null);
}
return getTrack(index);
}
public Track nextRandom() {
return getTrack((int) (Math.random() * playlist.size()));
}
private boolean equals(Object o1, Object o2) {
return (o1 != null && o1.equals(o2))
|| (o1 == null && o2 == null);
}
private Track prevPatternMatch(Track currentTrack, int index, Expression pattern, boolean shuffle) {
Track track;
Object result = pattern.eval(currentTrack);
track = prev(index);
if (track != null) {
if (equals(result, pattern.eval(track))) {
return track;
}
}
if (shuffle) {
return prevShuffle(currentTrack, false, pattern);
}
for (int i = index; i < playlist.size(); i++) {
track = playlist.get(i);
if (!equals(result, pattern.eval(track))) {
return prev(i);
}
}
return track;
}
private Track nextShuffle(Track currentTrack, boolean searchSeparators, Expression pattern) {
Track minRating = null;
Track minGreater = null;
Object patternValue = null;
for (Track track : playlist) {
if (track == currentTrack
|| (searchSeparators && track.getTrackData().getLocation() != null)
|| (!searchSeparators && track.getTrackData().getLocation() == null))
continue;
if (pattern != null) {
Object value = pattern.eval(track);
if (equals(patternValue, value)) {
continue;
}
patternValue = value;
}
if (minRating == null || track.getShuffleRating() < minRating.getShuffleRating()) {
minRating = track;
}
if (track.getShuffleRating() >= currentTrack.getShuffleRating()) {
if (minGreater == null || track.getShuffleRating() < minGreater.getShuffleRating()) {
minGreater = track;
}
}
}
return minGreater != null ? minGreater : minRating;
}
private Track prevShuffle(Track currentTrack, boolean searchSeparators, Expression pattern) {
Track maxSmaller = null;
Track maxRating = null;
Object patternValue = null;
for (Track track: playlist) {
if (track == currentTrack
|| (searchSeparators && track.getTrackData().getLocation() != null)
|| (!searchSeparators && track.getTrackData().getLocation() == null))
continue;
if (pattern != null) {
Object value = pattern.eval(track);
if (equals(patternValue, value)) {
continue;
}
patternValue = value;
}
if (maxRating == null || track.getShuffleRating() > maxRating.getShuffleRating()) {
maxRating = track;
}
if (track.getShuffleRating() <= currentTrack.getShuffleRating()) {
if (maxSmaller == null || track.getShuffleRating() > maxSmaller.getShuffleRating()) {
maxSmaller = track;
}
}
}
return maxSmaller != null ? maxSmaller : maxRating;
}
public boolean trackPlayable(Track track) {
return playlist.contains(track);
}
}