/*
This file is part of Libresonic.
Libresonic 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
(at your option) any later version.
Libresonic 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 Libresonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2016 (C) Libresonic Authors
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus
*/
package org.libresonic.player.domain;
import org.apache.commons.lang.StringUtils;
import java.io.IOException;
import java.util.*;
/**
* A play queue is a list of music files that are associated to a remote player.
*
* @author Sindre Mehus
*/
public class PlayQueue {
private List<MediaFile> files = new ArrayList<MediaFile>();
private boolean repeatEnabled;
private String name = "(unnamed)";
private Status status = Status.PLAYING;
private RandomSearchCriteria randomSearchCriteria;
/**
* The index of the current song, or -1 is the end of the playlist is reached.
* Note that both the index and the playlist size can be zero.
*/
private int index = 0;
/**
* Used for undo functionality.
*/
private List<MediaFile> filesBackup = new ArrayList<MediaFile>();
private int indexBackup = 0;
/**
* Returns the user-defined name of the playlist.
*
* @return The name of the playlist, or <code>null</code> if no name has been assigned.
*/
public synchronized String getName() {
return name;
}
/**
* Sets the user-defined name of the playlist.
*
* @param name The name of the playlist.
*/
public synchronized void setName(String name) {
this.name = name;
}
/**
* Returns the current song in the playlist.
*
* @return The current song in the playlist, or <code>null</code> if no current song exists.
*/
public synchronized MediaFile getCurrentFile() {
if (index == -1 || index == 0 && size() == 0) {
setStatus(Status.STOPPED);
return null;
} else {
MediaFile file = files.get(index);
// Remove file from playlist if it doesn't exist.
if (!file.exists()) {
files.remove(index);
index = Math.max(0, Math.min(index, size() - 1));
return getCurrentFile();
}
return file;
}
}
/**
* Returns all music files in the playlist.
*
* @return All music files in the playlist.
*/
public synchronized List<MediaFile> getFiles() {
return files;
}
/**
* Returns the music file at the given index.
*
* @param index The index.
* @return The music file at the given index.
* @throws IndexOutOfBoundsException If the index is out of range.
*/
public synchronized MediaFile getFile(int index) {
return files.get(index);
}
/**
* Skip to the next song in the playlist.
*/
public synchronized void next() {
index++;
// Reached the end?
if (index >= size()) {
index = isRepeatEnabled() ? 0 : -1;
}
}
/**
* Returns the number of songs in the playlists.
*
* @return The number of songs in the playlists.
*/
public synchronized int size() {
return files.size();
}
/**
* Returns whether the playlist is empty.
*
* @return Whether the playlist is empty.
*/
public synchronized boolean isEmpty() {
return files.isEmpty();
}
/**
* Returns the index of the current song.
*
* @return The index of the current song, or -1 if the end of the playlist is reached.
*/
public synchronized int getIndex() {
return index;
}
/**
* Sets the index of the current song.
*
* @param index The index of the current song.
*/
public synchronized void setIndex(int index) {
makeBackup();
this.index = Math.max(0, Math.min(index, size() - 1));
setStatus(Status.PLAYING);
}
/**
* Adds one or more music file to the playlist.
*
* @param mediaFiles The music files to add.
* @param index Where to add them.
* @throws IOException If an I/O error occurs.
*/
public synchronized void addFilesAt(Iterable<MediaFile> mediaFiles, int index) throws IOException {
makeBackup();
for (MediaFile mediaFile : mediaFiles) {
files.add(index, mediaFile);
index++;
}
setStatus(Status.PLAYING);
}
/**
* Adds one or more music file to the playlist.
*
* @param append Whether existing songs in the playlist should be kept.
* @param mediaFiles The music files to add.
* @throws IOException If an I/O error occurs.
*/
public synchronized void addFiles(boolean append, Iterable<MediaFile> mediaFiles) throws IOException {
makeBackup();
if (!append) {
index = 0;
files.clear();
}
for (MediaFile mediaFile : mediaFiles) {
files.add(mediaFile);
}
setStatus(Status.PLAYING);
}
/**
* Convenience method, equivalent to {@link #addFiles(boolean, Iterable)}.
*/
public synchronized void addFiles(boolean append, MediaFile... mediaFiles) throws IOException {
addFiles(append, Arrays.asList(mediaFiles));
}
/**
* Removes the music file at the given index.
*
* @param index The playlist index.
*/
public synchronized void removeFileAt(int index) {
makeBackup();
index = Math.max(0, Math.min(index, size() - 1));
if (this.index > index) {
this.index--;
}
files.remove(index);
if (index != -1) {
this.index = Math.max(0, Math.min(this.index, size() - 1));
}
}
/**
* Clears the playlist.
*/
public synchronized void clear() {
makeBackup();
files.clear();
index = 0;
}
/**
* Shuffles the playlist.
*/
public synchronized void shuffle() {
makeBackup();
MediaFile currentFile = getCurrentFile();
Collections.shuffle(files);
if (currentFile != null) {
Collections.swap(files, files.indexOf(currentFile), 0);
index = 0;
}
}
/**
* Sorts the playlist according to the given sort order.
*/
public synchronized void sort(final SortOrder sortOrder) {
makeBackup();
MediaFile currentFile = getCurrentFile();
Comparator<MediaFile> comparator = new Comparator<MediaFile>() {
public int compare(MediaFile a, MediaFile b) {
switch (sortOrder) {
case TRACK:
Integer trackA = a.getTrackNumber();
Integer trackB = b.getTrackNumber();
if (trackA == null) {
trackA = 0;
}
if (trackB == null) {
trackB = 0;
}
return trackA.compareTo(trackB);
case ARTIST:
String artistA = StringUtils.trimToEmpty(a.getArtist());
String artistB = StringUtils.trimToEmpty(b.getArtist());
return artistA.compareTo(artistB);
case ALBUM:
String albumA = StringUtils.trimToEmpty(a.getAlbumName());
String albumB = StringUtils.trimToEmpty(b.getAlbumName());
return albumA.compareTo(albumB);
default:
return 0;
}
}
};
Collections.sort(files, comparator);
if (currentFile != null) {
index = files.indexOf(currentFile);
}
}
/**
* Rearranges the playlist using the provided indexes.
*/
public synchronized void rearrange(int[] indexes) {
makeBackup();
if (indexes == null || indexes.length != size()) {
return;
}
MediaFile[] newFiles = new MediaFile[files.size()];
for (int i = 0; i < indexes.length; i++) {
newFiles[i] = files.get(indexes[i]);
}
for (int i = 0; i < indexes.length; i++) {
if (index == indexes[i]) {
index = i;
break;
}
}
files.clear();
files.addAll(Arrays.asList(newFiles));
}
/**
* Moves the song at the given index one step up.
*
* @param index The playlist index.
*/
public synchronized void moveUp(int index) {
makeBackup();
if (index <= 0 || index >= size()) {
return;
}
Collections.swap(files, index, index - 1);
if (this.index == index) {
this.index--;
} else if (this.index == index - 1) {
this.index++;
}
}
/**
* Moves the song at the given index one step down.
*
* @param index The playlist index.
*/
public synchronized void moveDown(int index) {
makeBackup();
if (index < 0 || index >= size() - 1) {
return;
}
Collections.swap(files, index, index + 1);
if (this.index == index) {
this.index++;
} else if (this.index == index + 1) {
this.index--;
}
}
/**
* Returns whether the playlist is repeating.
*
* @return Whether the playlist is repeating.
*/
public synchronized boolean isRepeatEnabled() {
return repeatEnabled;
}
/**
* Sets whether the playlist is repeating.
*
* @param repeatEnabled Whether the playlist is repeating.
*/
public synchronized void setRepeatEnabled(boolean repeatEnabled) {
this.repeatEnabled = repeatEnabled;
}
/**
* Returns whether the playlist is a shuffle radio
*
* @return Whether the playlist is a shuffle radio.
*/
public synchronized boolean isRadioEnabled() {
return this.randomSearchCriteria != null;
}
/**
* Revert the last operation.
*/
public synchronized void undo() {
List<MediaFile> filesTmp = new ArrayList<MediaFile>(files);
int indexTmp = index;
index = indexBackup;
files = filesBackup;
indexBackup = indexTmp;
filesBackup = filesTmp;
}
/**
* Returns the playlist status.
*
* @return The playlist status.
*/
public synchronized Status getStatus() {
return status;
}
/**
* Sets the playlist status.
*
* @param status The playlist status.
*/
public synchronized void setStatus(Status status) {
this.status = status;
if (index == -1) {
index = Math.max(0, Math.min(index, size() - 1));
}
}
/**
* Returns the criteria used to generate this random playlist.
*
* @return The search criteria, or <code>null</code> if this is not a random playlist.
*/
public synchronized RandomSearchCriteria getRandomSearchCriteria() {
return randomSearchCriteria;
}
/**
* Sets the criteria used to generate this random playlist.
*
* @param randomSearchCriteria The search criteria, or <code>null</code> if this is not a random playlist.
*/
public synchronized void setRandomSearchCriteria(RandomSearchCriteria randomSearchCriteria) {
this.randomSearchCriteria = randomSearchCriteria;
}
/**
* Returns the total length in bytes.
*
* @return The total length in bytes.
*/
public synchronized long length() {
long length = 0;
for (MediaFile mediaFile : files) {
length += mediaFile.getFileSize();
}
return length;
}
private void makeBackup() {
filesBackup = new ArrayList<MediaFile>(files);
indexBackup = index;
}
/**
* Playlist status.
*/
public enum Status {
PLAYING,
STOPPED
}
/**
* Playlist sort order.
*/
public enum SortOrder {
TRACK,
ARTIST,
ALBUM
}
}