/*
* The GPLv3 licence :
* -----------------
* Copyright (c) 2009 Ricardo Dias
*
* This file is part of MuVis.
*
* MuVis 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.
*
* MuVis 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 MuVis. If not, see <http://www.gnu.org/licenses/>.
*/
package muvis.audio.playlist;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import java.util.Vector;
import muvis.audio.AudioMetadata;
import muvis.audio.AudioMetadataExtractor;
import muvis.audio.MP3AudioMetadataExtractor;
import muvis.util.Observer;
import muvis.util.Util;
public class BasePlaylist implements Playlist {
/**
* Vector that holds the playlist items
*/
protected Vector<PlaylistItem> playlist;
/**
* Vector that holds the observers of this playlist
*/
protected Vector<Observer> observers;
/**
* Index that indicates the currently selected item
*/
protected int cursor;
/**
* Item associated with the current cursor (for internal purposes)
*/
protected PlaylistItem cursorItem;
/**
* Indicates if the playlist has changed: new items added
*/
protected boolean isModified;
/**
* String that represents the current directory of this playlist
*/
protected String currentDirectory;
/**
* This event is used to update the observers
*/
private Event event;
/**
* Constructs an object of type BasePlaylist
* @param base The PApplet necessary for this playlist to be able to load metadata from the music file items
*/
public BasePlaylist() {
playlist = new Vector<PlaylistItem>();
observers = new Vector<Observer>();
currentDirectory = "";
cursor = 0;
cursorItem = null;
event = Event.NOTHING;
}
/**
* Sets the Current Directory of the Playlist to the parameter
* @param directory
*/
@Override
public void setCurrentDirectory(String directory) {
currentDirectory = directory;
}
/**
* Gets the Current Directory of the Playlist
* @return currentDirectory
*/
@Override
public String getCurrentDirectory() {
return currentDirectory;
}
/**
* Add a playlist item to a specific location in the playlist internal
* structure.
* @param pli
* @param pos
*/
@Override
public void addItemAt(PlaylistItem pli, int pos) {
//The item can only be added if that index is valid
if (pos >= 0 && pos < playlist.size()) {
playlist.add(pos, pli);
isModified = true;
recalculateCursor();
event = Event.PLAYLIST_RESIZED;
updateObservers();
}
}
@Override
public void addItemsAt(Map<Integer, PlaylistItem> items) {
for (Map.Entry<Integer, PlaylistItem> it : items.entrySet()){
if (it.getKey() >= 0 && it.getKey() < playlist.size()) {
playlist.add(it.getKey(), it.getValue());
}
}
isModified = true;
recalculateCursor();
event = Event.PLAYLIST_RESIZED;
updateObservers();
}
@Override
public void removeItemsAt(ArrayList<Integer> positions) {
throw new UnsupportedOperationException("Not supported yet.");
}
/**
* Append a new item to the playlist
* @param pli
*/
@Override
public void appendItem(PlaylistItem pli) {
playlist.add(pli);
isModified = true;
event = Event.PLAYLIST_RESIZED;
updateObservers();
}
/**
* Appends the specified items to the playlist
* @param items
*/
@Override
public void appendItems(ArrayList<PlaylistItem> items) {
for (PlaylistItem it : items){
playlist.add((it));
}
isModified = true;
event = Event.PLAYLIST_RESIZED;
updateObservers();
}
/**
* Restarts the cursor of the playlist
*/
@Override
public void begin() {
cursor = 0;
if (playlist.size() > 0){
cursorItem = playlist.firstElement();
} else cursorItem = null;
event = Event.NEW_CURSOR;
updateObservers();
}
/**
* Gets all the items in this playlist
* @return
*/
@Override
public Collection<PlaylistItem> getAllItems() {
return playlist;
}
/**
* Gets the currently selected playlist item
* @return
*/
@Override
public PlaylistItem getCursor() {
return playlist.get(cursor);
}
/**
* This specific method returns the index of pli, but if the playlist doesn't have this item, returns -1
*/
@Override
public int getIndex(PlaylistItem pli) {
if (playlist.contains(pli)) {
return playlist.indexOf(pli);
} else {
return -1;
}
}
/**
* Returns null if there isn't an item at position pos
*/
@Override
public PlaylistItem getItemAt(int pos) {
if (pos >= 0 && pos < playlist.size()) {
return playlist.elementAt(pos);
} else {
return null;
}
}
/**
* Gets playlist size
* @return
*/
@Override
public int getPlaylistSize() {
return playlist.size();
}
/**
* Gets the selected index
* @return
*/
@Override
public int getSelectedIndex() {
return cursor;
}
/**
* Indicates if the playlist has changed
* @return
*/
@Override
public boolean isModified() {
return isModified;
}
/**
* Loads playlist as M3U format.
* @return true if the playlist was correctly loaded or false otherwise
*/
@Override
public boolean load(String filename, String directory) {
setModified(true);
boolean loaded = false;
if ((filename != null) && (filename.toLowerCase().endsWith(".m3u"))) {
loaded = loadM3U(filename, directory);
} else {
System.out.println("Can't load the playlist");
}
if (loaded) {
event = Event.PLAYLIST_LOADED;
updateObservers();
}
return loaded;
}
/**
* Load playlist from M3U format.
*
* @param filename
* @return
*/
protected boolean loadM3U(String filename, String directory) {
AudioMetadataExtractor metatadaExtractor = new MP3AudioMetadataExtractor();
boolean loaded = false;
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(directory + filename));
String line = null;
String songName = null;
String songFile = null;
while ((line = br.readLine()) != null) {
if (line.trim().length() == 0) {
continue;
}
if (line.startsWith("#")) {
if (line.toUpperCase().startsWith("#EXTINF"));
} else {
songFile = line;
if (songName == null) {
songName = songFile;
}
PlaylistItem pli = null;
// File.
//Try relative path
File f = new File(directory + songName);
if (f.exists()) {
AudioMetadata meta = metatadaExtractor.getAudioMetadata(directory + songName);
pli = new PlaylistItem(songName, directory, meta);
} else {
// Try absolute path
f = new File(songName);
if (f.exists()) {
AudioMetadata meta = metatadaExtractor.getAudioMetadata(songName);
String path = f.getParent() + Util.getOSEscapeSequence();
CharSequence pathSeq = path.subSequence(0, path.length());
songName = songName.replace(pathSeq, "");
pli = new PlaylistItem(songName,
path, meta);
}
}
if (pli != null) {
this.appendItem(pli);
}
songFile = null;
songName = null;
}
loaded = true;
}
} catch (Exception e) {
// this here should give user a nice message
e.printStackTrace();
} finally {
try {
if (br != null) {
br.close();
}
} catch (Exception ioe) {
System.out.println("Can't close .m3u playlist\n" + ioe.toString());
}
}
return loaded;
}
/**
* Next cursor
*/
@Override
public void nextCursor() {
cursor++;
if (cursor >= playlist.size()) {
cursor = 0;
}
if (playlist.size() > 0){
cursorItem = playlist.get(cursor);
} else cursorItem = null;
event = Event.NEW_CURSOR;
updateObservers();
}
@Override
public void previousCursor() {
cursor--;
if (cursor < 0) {
cursor = playlist.size() - 1;
}
if (playlist.size() > 0){
cursorItem = playlist.get(cursor);
} else cursorItem = null;
event = Event.NEW_CURSOR;
updateObservers();
}
@Override
public void removeAllItems() {
playlist.clear();
event = Event.PLAYLIST_RESIZED;
updateObservers();
cursor = 0;
cursorItem = null;
}
@Override
public void removeItem(PlaylistItem pli) {
if (playlist.contains(pli)) { //only removes the item if it effectively exists
playlist.remove(pli);
recalculateCursor();
event = Event.PLAYLIST_RESIZED;
updateObservers();
}
}
private void recalculateCursor(){
boolean found = false;
for (PlaylistItem it : playlist){
if(cursorItem != null && it.equals(cursorItem)){
found = true;
cursor = getIndex(cursorItem);
break;
}
}
if (!found){
cursor = 0;
if (playlist.size() > 0){
cursorItem = getItemAt(cursor);
} else cursorItem = null;
}
}
@Override
public void removeItems(ArrayList<PlaylistItem> items) {
for (PlaylistItem it : items){
if (playlist.contains(it)){
playlist.remove(it);
}
}
recalculateCursor();
event = Event.PLAYLIST_RESIZED;
updateObservers();
}
@Override
public void removeItemAt(int pos) {
if (pos >= 0 && pos < playlist.size()) { //checks if the position is valid
playlist.remove(pos);
recalculateCursor();
event = Event.PLAYLIST_RESIZED;
updateObservers();
}
}
/**
* Saves playlist in M3U format.
* For each entry in the playlist, this method saves the full location of the file.
* This depends on the plataform used to save the playlist.
*/
@Override
public boolean save(String filename, String directory) {
if (playlist != null) {
BufferedWriter bw = null;
try {
bw = new BufferedWriter(new FileWriter(directory + filename + ".m3u"));
bw.write("#Playlist created by MuVis: because music visualization matters!");
bw.newLine();
bw.write("#EXTM3U");
bw.newLine();
Iterator<PlaylistItem> it = playlist.iterator();
while (it.hasNext()) {
bw.write("#EXTINF:");
PlaylistItem pli = (PlaylistItem) it.next();
int seconds = (int) Math.floor((pli.getLength() * 0.001));
bw.write(seconds + "," + pli.getArtistTrackName());
bw.newLine();
bw.write(pli.getLocation() + pli.getName());
bw.newLine();
}
event = Event.PLAYLIST_SAVED;
updateObservers();
return true;
} catch (IOException e) {
System.out.println("Can't save playlist" + e.toString());
} finally {
try {
if (bw != null) {
bw.close();
}
} catch (IOException ioe) {
System.out.println("Can't close playlist." + ioe.toString());
}
}
}
return false;
}
@Override
public void setCursor(int index) {
//The cursor must be clamped to a valid position
if (index < 0 || index >= playlist.size()) {
cursor = 0;
if (playlist.size() > 0){
cursorItem = playlist.firstElement();
} else cursorItem = null;
} else {
cursor = index;
cursorItem = playlist.get(index);
}
event = Event.NEW_CURSOR;
updateObservers();
}
@Override
public void updateCursor(int index) {
//The cursor must be clamped to a valid position
if (index < 0 || index >= playlist.size()) {
cursor = 0;
if (playlist.size() > 0){
cursorItem = playlist.firstElement();
} else cursorItem = null;
} else {
cursor = index;
cursorItem = playlist.get(index);
}
event = Event.PLAYLIST_UPDATED;
updateObservers();
}
@Override
public boolean setModified(boolean set) {
isModified = set;
return set;
}
@Override
/**
* Method that shuffles the playlist
*/
public void shuffle() {
PlaylistItemComparator comparator = new PlaylistItemComparator(0);
Collections.sort(playlist, comparator);
event = Event.GENERAL_MODIFIED;
updateObservers();
}
@Override
/**
* Method for sorting the playlist, acording to some sortmodes
*/
public void sortItems(int sortmode) {
PlaylistItemComparator comparator = new PlaylistItemComparator(sortmode);
Collections.sort(playlist, comparator);
event = Event.GENERAL_MODIFIED;
updateObservers();
}
@Override
public void registerObserver(Observer obs) {
observers.add(obs);
}
@Override
public void unregisterObserver(Observer obs) {
observers.remove(obs);
}
@Override
public void updateObservers() {
for (Observer o : observers) {
o.update(this, event);
}
}
@Override
public long getTotalPlayingTime() {
long totalTime = 0; //seconds
for (PlaylistItem item : playlist) {
totalTime += item.getLength();
}
return totalTime;
}
}
/**
* Simple playlist comparator just for shuffle and simple ordering
* Implements Comparator<PlaylistItem> because it comparates playlist items
* @author Ricardo
*/
class PlaylistItemComparator implements Comparator<PlaylistItem> {
private int sortmode;
private Random rand;
@Override
public int compare(PlaylistItem pli1, PlaylistItem pli2) {
int result = 0;
if (sortmode == -1) {
result = pli1.getName().compareTo(pli2.getName());
} else if (sortmode == 0) {
result = rand.nextInt(2);
if (result > 1) //if we have a value higher than 1, then use inverse order
{
result = -1;
}
} else {
result = pli1.getName().compareTo(pli2.getName());
if (result == 1) {
result = 0;
} else if (result == -1) {
result = 1;
}
}
return result;
}
/**
* Valid sortmodes: -1, 0, 1
* -1: descendente
* 0: random
* 1: ascendente
* @param sortmode
*/
public PlaylistItemComparator(int sortmode) {
if (sortmode > 1 || sortmode < -1) {
this.sortmode = 0; //random by default
} else {
this.sortmode = sortmode;
}
rand = new Random();
}
}