Added a queue system.

This commit is contained in:
Markil3
2021-08-03 13:11:36 -06:00
parent 8386c9575c
commit 52262b5668
12 changed files with 1912 additions and 352 deletions

View File

@@ -443,22 +443,25 @@ public class LocalSongProvider implements SongProvider<LocalSong>
{
try
{
state = getDb().createStatement();
result = state.executeQuery("SELECT mod FROM local_songs WHERE file='" + this.file.getAbsolutePath() + "';");
if (result.next())
synchronized (db)
{
if (result.getLong(1) >= this.file.lastModified())
state = getDb().createStatement();
result = state.executeQuery("SELECT mod FROM local_songs WHERE file='" + this.file.getAbsolutePath().replaceAll("'", "''") + "';");
if (result.next())
{
/*
* No modifications needed
*/
updatedSongs++;
triggerUpdateListeners();
return;
}
else
{
state.executeUpdate("UPDATE local_songs SET mod = " + this.file.lastModified() + " WHERE file='" + this.file.getAbsolutePath() + "';");
if (result.getLong(1) >= this.file.lastModified())
{
/*
* No modifications needed
*/
updatedSongs++;
triggerUpdateListeners();
return;
}
else
{
state.executeUpdate("UPDATE local_songs SET mod = " + this.file.lastModified() + " WHERE file='" + this.file.getAbsolutePath().replaceAll("'", "''") + "';");
}
}
}
currentFolder = file.getPath();
@@ -609,147 +612,153 @@ public class LocalSongProvider implements SongProvider<LocalSong>
/*
* Update album information.
*/
result = state.executeQuery("SELECT album FROM local_albums WHERE album='" + albumTitle + "';");
if (!result.next())
synchronized (db)
{
state.executeUpdate("INSERT INTO local_albums (album) VALUES ('" + albumTitle + "');");
}
result = state.executeQuery("SELECT artists FROM local_albums WHERE album='" + albumTitle + "';");
if (result.getString("artists") == null && albumArtist != null)
{
state.executeUpdate("UPDATE local_albums SET artists='" + Arrays.stream(albumArtist.split(";")).map(String::trim).collect(Collectors.joining(";")) + "' WHERE album='" + albumTitle + "';");
}
// TODO - Can we get year metadata?
result = state.executeQuery("SELECT genres FROM local_albums WHERE album='" + albumTitle + "';");
if (result.getString("genres") == null && genre != null)
{
state.executeUpdate("UPDATE local_albums SET genres='" + Arrays.stream(genre.split(";")).map(String::trim).collect(Collectors.joining(";")) + "' WHERE album='" + albumTitle + "';");
}
result = state.executeQuery("SELECT tracks FROM local_albums WHERE album='" + albumTitle + "';");
if (result.getInt("tracks") == 0 && track != null && track[1] > 0)
{
state.executeUpdate("UPDATE local_albums SET tracks=" + track[1] + " WHERE album='" + albumTitle + "';");
}
result = state.executeQuery("SELECT discs FROM local_albums WHERE album='" + albumTitle + "';");
if (result.getInt("discs") == 0 && disc != null && disc[1] > 0)
{
state.executeUpdate("UPDATE local_albums SET tracks=" + disc[1] + " WHERE album='" + albumTitle + "';");
}
/*
* Create the song
*/
result = state.executeQuery("SELECT title FROM local_songs WHERE file='" + file.getAbsolutePath() + "';");
if (result.next())
{
logger.debug("Updating song cache for {} ({})", title, file);
StringBuilder sql = new StringBuilder("UPDATE local_songs SET ");
sql.append("codec='").append(codec).append("', ");
sql.append("type='").append(codec).append("', ");
if (title != null && !title.isEmpty())
/*
* This part in particular is prone to thread-safety issues.
*/
result = state.executeQuery("SELECT album FROM local_albums WHERE album='" + albumTitle + "';");
if (!result.next())
{
sql.append("title='").append(title).append("', ");
state.executeUpdate("INSERT INTO local_albums (album) VALUES ('" + albumTitle + "');");
}
else
result = state.executeQuery("SELECT artists FROM local_albums WHERE album='" + albumTitle + "';");
if (result.getString("artists") == null && albumArtist != null)
{
sql.append("title='").append(file.getName()).append("', ");
state.executeUpdate("UPDATE local_albums SET artists='" + Arrays.stream(albumArtist.split(";")).map(String::trim).collect(Collectors.joining(";")) + "' WHERE album='" + albumTitle + "';");
}
if (artist != null && !artist.isEmpty())
// TODO - Can we get year metadata?
result = state.executeQuery("SELECT genres FROM local_albums WHERE album='" + albumTitle + "';");
if (result.getString("genres") == null && genre != null)
{
sql.append("artists='").append(Optional.of(artist).map(s -> s.split(";")).stream().flatMap(Arrays::stream).map(String::trim).collect(Collectors.joining(";"))).append("', ");
state.executeUpdate("UPDATE local_albums SET genres='" + Arrays.stream(genre.split(";")).map(String::trim).collect(Collectors.joining(";")) + "' WHERE album='" + albumTitle + "';");
}
else
result = state.executeQuery("SELECT tracks FROM local_albums WHERE album='" + albumTitle + "';");
if (result.getInt("tracks") == 0 && track != null && track[1] > 0)
{
sql.append("artists=NULL, ");
state.executeUpdate("UPDATE local_albums SET tracks=" + track[1] + " WHERE album='" + albumTitle + "';");
}
if (track != null && track[0] > 0)
result = state.executeQuery("SELECT discs FROM local_albums WHERE album='" + albumTitle + "';");
if (result.getInt("discs") == 0 && disc != null && disc[1] > 0)
{
sql.append("track=").append(track[0]).append(", ");
}
else
{
sql.append("track=NULL, ");
}
if (disc != null && disc[0] > 0)
{
sql.append("disc=").append(disc[0]).append(", ");
}
else
{
sql.append("disc=NULL, ");
}
if (duration != 0)
{
sql.append("duration=").append(duration).append(", ");
}
else
{
sql.append("duration=NULL, ");
}
if (albumTitle != null && !albumTitle.isEmpty())
{
sql.append("album='").append(albumTitle).append("', ");
}
else
{
sql.append("album=NULL, ");
}
sql.append("mod=").append(file.lastModified());
sql.append(" WHERE file='").append(file.getAbsolutePath()).append("';");
state.executeUpdate(sql.toString());
}
else
{
logger.debug("Caching song {} ({})", title, file);
StringBuilder sql = new StringBuilder("INSERT INTO local_songs ");
StringBuilder columns = new StringBuilder("(");
StringBuilder values = new StringBuilder("(");
columns.append("file,");
values.append('\'').append(file.getAbsolutePath()).append("',");
columns.append("codec,");
values.append('\'').append(codec).append("',");
columns.append("type,");
values.append('\'').append(type).append("',");
if (title != null && !title.isEmpty())
{
columns.append("title,");
values.append('\'').append(title).append("',");
}
if (artist != null && !artist.isEmpty())
{
columns.append("artists,");
values.append('\'').append(Optional.of(artist).map(s -> s.split(";")).stream().flatMap(Arrays::stream).map(String::trim).collect(Collectors.joining(";"))).append("',");
}
if (track != null && track[0] > 0)
{
columns.append("track,");
values.append(track[0]).append(",");
}
if (disc != null && disc[0] > 0)
{
columns.append("disc,");
values.append(disc[0]).append(",");
}
if (duration > 0)
{
columns.append("duration,");
values.append(duration).append(",");
}
if (albumTitle != null && !albumTitle.isEmpty())
{
columns.append("album,");
values.append('\'').append(albumTitle).append("',");
state.executeUpdate("UPDATE local_albums SET tracks=" + disc[1] + " WHERE album='" + albumTitle + "';");
}
columns.append("mod");
values.append(file.lastModified());
columns.append(") VALUES ");
values.append(");");
sql.append(columns);
sql.append(values);
state.executeUpdate(sql.toString());
/*
* Create the song
*/
result = state.executeQuery("SELECT title FROM local_songs WHERE file='" + file.getAbsolutePath().replaceAll("'", "''") + "';");
if (result.next())
{
logger.debug("Updating song cache for {} ({})", title, file);
StringBuilder sql = new StringBuilder("UPDATE local_songs SET ");
sql.append("codec='").append(codec).append("', ");
sql.append("type='").append(codec).append("', ");
if (title != null && !title.isEmpty())
{
sql.append("title='").append(title).append("', ");
}
else
{
sql.append("title='").append(file.getName()).append("', ");
}
if (artist != null && !artist.isEmpty())
{
sql.append("artists='").append(Optional.of(artist).map(s -> s.split(";")).stream().flatMap(Arrays::stream).map(String::trim).collect(Collectors.joining(";"))).append("', ");
}
else
{
sql.append("artists=NULL, ");
}
if (track != null && track[0] > 0)
{
sql.append("track=").append(track[0]).append(", ");
}
else
{
sql.append("track=NULL, ");
}
if (disc != null && disc[0] > 0)
{
sql.append("disc=").append(disc[0]).append(", ");
}
else
{
sql.append("disc=NULL, ");
}
if (duration != 0)
{
sql.append("duration=").append(duration).append(", ");
}
else
{
sql.append("duration=NULL, ");
}
if (albumTitle != null && !albumTitle.isEmpty())
{
sql.append("album='").append(albumTitle).append("', ");
}
else
{
sql.append("album=NULL, ");
}
sql.append("mod=").append(file.lastModified());
sql.append(" WHERE file='").append(file.getAbsolutePath().replaceAll("'", "''")).append("';");
state.executeUpdate(sql.toString());
}
else
{
logger.debug("Caching song {} ({})", title, file);
StringBuilder sql = new StringBuilder("INSERT INTO local_songs ");
StringBuilder columns = new StringBuilder("(");
StringBuilder values = new StringBuilder("(");
columns.append("file,");
values.append('\'').append(file.getAbsolutePath().replaceAll("'", "''")).append("',");
columns.append("codec,");
values.append('\'').append(codec).append("',");
columns.append("type,");
values.append('\'').append(type).append("',");
if (title != null && !title.isEmpty())
{
columns.append("title,");
values.append('\'').append(title).append("',");
}
if (artist != null && !artist.isEmpty())
{
columns.append("artists,");
values.append('\'').append(Optional.of(artist).map(s -> s.split(";")).stream().flatMap(Arrays::stream).map(String::trim).collect(Collectors.joining(";"))).append("',");
}
if (track != null && track[0] > 0)
{
columns.append("track,");
values.append(track[0]).append(",");
}
if (disc != null && disc[0] > 0)
{
columns.append("disc,");
values.append(disc[0]).append(",");
}
if (duration > 0)
{
columns.append("duration,");
values.append(duration).append(",");
}
if (albumTitle != null && !albumTitle.isEmpty())
{
columns.append("album,");
values.append('\'').append(albumTitle).append("',");
}
columns.append("mod");
values.append(file.lastModified());
columns.append(") VALUES ");
values.append(");");
sql.append(columns);
sql.append(values);
state.executeUpdate(sql.toString());
}
}
updatedSongs++;
@@ -828,83 +837,86 @@ public class LocalSongProvider implements SongProvider<LocalSong>
/*
* Check if the table exists
*/
state = getDb().createStatement();
result = state.executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='LOCAL_ALBUMS';");
if (!result.next())
synchronized (db)
{
logger.debug("Creating album table.");
/*
* Create the table
*/
state.executeUpdate("CREATE TABLE LOCAL_ALBUMS" +
"(ALBUM TEXT PRIMARY KEY NOT NULL," +
"ARTISTS TEXT," +
"YEAR INTEGER," +
"GENRES TEXT," +
"TRACKS INTEGER," +
"DISCS INTEGER);");
}
else
{
result = state.executeQuery("SELECT * FROM LOCAL_ALBUMS;");
while (result.next())
state = getDb().createStatement();
result = state.executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='LOCAL_ALBUMS';");
if (!result.next())
{
album = albums.get(result.getString("album"));
if (album == null)
{
album = new Album();
album.name = result.getString("album");
albums.put(album.name, album);
}
album.artists = Optional.ofNullable(result.getString("artists")).map(s -> s.split(";")).orElse(new String[0]);
album.year = result.getInt("year");
album.genres = Optional.ofNullable(result.getString("genres")).map(s -> s.split(";")).orElse(new String[0]);
album.totalTracks = result.getInt("tracks");
album.totalDiscs = result.getInt("discs");
logger.debug("Creating album table.");
/*
* Create the table
*/
state.executeUpdate("CREATE TABLE LOCAL_ALBUMS" +
"(ALBUM TEXT PRIMARY KEY NOT NULL," +
"ARTISTS TEXT," +
"YEAR INTEGER," +
"GENRES TEXT," +
"TRACKS INTEGER," +
"DISCS INTEGER);");
}
}
result = state.executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='LOCAL_SONGS';");
if (!result.next())
{
logger.debug("Creating song table.");
/*
* Create the table
*/
state.executeUpdate("CREATE TABLE LOCAL_SONGS" +
"(FILE TEXT PRIMARY KEY NOT NULL," +
"CODEC CHAR(5)," +
"TYPE CHAR(5)," +
"TITLE TEXT," +
"ARTISTS TEXT," +
"TRACK INTEGER," +
"DISC INTEGER," +
"DURATION BIGINT," +
"ALBUM TEXT," +
"MOD BIGINT);");
}
else
{
result = state.executeQuery("SELECT * FROM LOCAL_SONGS;");
while (result.next())
else
{
song = songs.get(new File(result.getString("file")));
if (song == null)
result = state.executeQuery("SELECT * FROM LOCAL_ALBUMS;");
while (result.next())
{
song = new LocalSong();
song.file = new File(result.getString("file"));
songs.put(song.file, song);
album = albums.get(result.getString("album"));
if (album == null)
{
album = new Album();
album.name = result.getString("album");
albums.put(album.name, album);
}
album.artists = Optional.ofNullable(result.getString("artists")).map(s -> s.split(";")).orElse(new String[0]);
album.year = result.getInt("year");
album.genres = Optional.ofNullable(result.getString("genres")).map(s -> s.split(";")).orElse(new String[0]);
album.totalTracks = result.getInt("tracks");
album.totalDiscs = result.getInt("discs");
}
song.codec = result.getString("codec");
song.type = result.getString("type");
song.title = result.getString("title");
song.artists = Optional.ofNullable(result.getString("artists")).map(s -> s.split(";")).orElse(new String[0]);
song.trackNum = result.getInt("track");
song.disc = result.getInt("disc");
song.duration = result.getLong("duration");
song.album = Optional.ofNullable(result.getString("album")).map(albums::get).orElse(null);
}
result = state.executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='LOCAL_SONGS';");
if (!result.next())
{
logger.debug("Creating song table.");
/*
* Create the table
*/
state.executeUpdate("CREATE TABLE LOCAL_SONGS" +
"(FILE TEXT PRIMARY KEY NOT NULL," +
"CODEC CHAR(5)," +
"TYPE CHAR(5)," +
"TITLE TEXT," +
"ARTISTS TEXT," +
"TRACK INTEGER," +
"DISC INTEGER," +
"DURATION BIGINT," +
"ALBUM TEXT," +
"MOD BIGINT);");
}
else
{
result = state.executeQuery("SELECT * FROM LOCAL_SONGS;");
while (result.next())
{
song = songs.get(new File(result.getString("file")));
if (song == null)
{
song = new LocalSong();
song.file = new File(result.getString("file"));
songs.put(song.file, song);
}
song.codec = result.getString("codec");
song.type = result.getString("type");
song.title = result.getString("title");
song.artists = Optional.ofNullable(result.getString("artists")).map(s -> s.split(";")).orElse(new String[0]);
song.trackNum = result.getInt("track");
song.disc = result.getInt("disc");
song.duration = result.getLong("duration");
song.album = Optional.ofNullable(result.getString("album")).map(albums::get).orElse(null);
}
}
state.close();
}
state.close();
}
catch (SQLException e)
{

View File

@@ -0,0 +1,469 @@
/*
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
*/
package edu.regis.universeplayer.data;
import java.util.*;
/**
* A song queue contains a list of songs that are to play, along with a way to
* order them
*
* @author William Hubbard
* @version 0.1
*/
public class Queue extends ArrayList<Song>
{
private static Queue INSTANCE;
public static Queue getInstance()
{
if (INSTANCE == null)
{
INSTANCE = new Queue();
}
return INSTANCE;
}
private final ArrayList<Song> queueOrder = new ArrayList<>();
private int currentIndex;
private boolean shuffle;
private boolean repeat;
private final Random random = new Random();
private final LinkedList<SongChangeListener> songListeners = new LinkedList<>();
private final LinkedList<QueueChangeListener> queueListeners = new LinkedList<>();
public Queue()
{
}
/**
* Obtains the song scheduled to play.
*
* @return The currently playing song.
*/
public Song getCurrentSong()
{
if (this.currentIndex < 0 || this.currentIndex >= this.queueOrder.size())
{
return null;
}
return this.queueOrder.get(currentIndex);
}
/**
* Obtains the song index scheduled to play.
*
* @return The currently playing song index.
*/
public int getCurrentIndex()
{
return this.indexOf(this.getCurrentSong());
}
/**
* Checks whether this queue will loop after the end of the song.
*
* @return True if the queue loops, false otherwise.
*/
public boolean isRepeating()
{
return this.repeat;
}
/**
* Sets whether this queue should loop after the last song plays.
*
* @param repeat - Whether the queue loops.
*/
public void setRepeat(boolean repeat)
{
this.repeat = repeat;
}
/**
* Checks whether this queue is in shuffle mode.
*
* @return True if the queue shuffles, false otherwise.
*/
public boolean isShuffling()
{
return this.shuffle;
}
/**
* Sets whether this queue should be shuffled or not.
*
* @param shuffle - Whether or not the queue shuffles.
*/
public void setShuffle(boolean shuffle)
{
this.shuffle = shuffle;
this.queueOrder.clear();
}
/**
* Skips to the next song.
*
* @return The new song, or null if we finished the list.
*/
public Song skipNext()
{
if (++this.currentIndex >= this.queueOrder.size())
{
if (this.repeat)
{
this.currentIndex = 0;
/*
* Reshuffle the queue as needed.
*/
this.queueOrder.clear();
this.getOrder();
this.triggerSongChangeListeners();
return this.getCurrentSong();
}
else
{
/*
* Make sure we aren't infinitely increasing the current index.
*/
this.currentIndex = this.queueOrder.size() - 1;
this.triggerSongChangeListeners();
return null;
}
}
this.triggerSongChangeListeners();
return this.getCurrentSong();
}
/**
* Skips to the previous song.
*
* @return The previous song, or the first one if we go underboard.
*/
public Song skipPrev()
{
if (--this.currentIndex < 0)
{
if (this.repeat)
{
this.currentIndex = this.queueOrder.size() - 1;
}
else
{
this.currentIndex = 0;
}
}
this.triggerSongChangeListeners();
return this.getCurrentSong();
}
/**
* Skips to the song at the provided song index.
*
* @param index - The new index. This is not relational to the queue.
* @return The new song, or null if we finished the list.
*/
public Song skipToSong(int index)
{
Song song = this.get(index);
this.currentIndex = this.queueOrder.indexOf(song);
this.triggerSongChangeListeners();
return this.getCurrentSong();
}
private ArrayList<Song> getOrder()
{
LinkedList<Song> indexesRemaining;
if (this.queueOrder.isEmpty())
{
this.queueOrder.addAll(this);
if (this.shuffle)
{
indexesRemaining = new LinkedList<>(this.queueOrder);
this.queueOrder.clear();
while (indexesRemaining.size() > 0)
{
this.queueOrder.add(indexesRemaining.remove(random.nextInt(indexesRemaining.size())));
}
}
}
return this.queueOrder;
}
@Override
public boolean add(Song song)
{
int newIndex;
if (super.add(song))
{
if (this.shuffle)
{
newIndex = random.nextInt(this.queueOrder.size());
if (newIndex <= this.currentIndex)
{
this.currentIndex++;
}
this.queueOrder.add(newIndex, song);
}
else
{
this.queueOrder.add(song);
}
this.triggerQueueChangeListeners();
return true;
}
return false;
}
@Override
public void add(int index, Song song)
{
int newIndex;
super.add(index, song);
if (this.shuffle)
{
newIndex = this.random.nextInt(this.queueOrder.size());
}
else
{
newIndex = index;
}
if (newIndex <= this.currentIndex)
{
this.currentIndex++;
}
this.queueOrder.add(newIndex, song);
this.triggerQueueChangeListeners();
}
@Override
public Song remove(int index)
{
Song removed = super.remove(index);
if (removed != null)
{
index = this.queueOrder.indexOf(removed);
if (index <= this.currentIndex)
{
this.currentIndex--;
}
this.queueOrder.remove(removed);
this.triggerQueueChangeListeners();
}
return removed;
}
@Override
public boolean remove(Object o)
{
int index = this.indexOf(o);
if (super.remove(o))
{
if (index <= this.currentIndex)
{
this.currentIndex--;
}
this.queueOrder.remove(o);
this.triggerQueueChangeListeners();
return true;
}
return false;
}
@Override
public void clear()
{
super.clear();
this.queueOrder.clear();
this.currentIndex = 0;
this.triggerQueueChangeListeners();
this.triggerSongChangeListeners();
}
@Override
public boolean addAll(Collection<? extends Song> c)
{
int newIndex;
int length = this.size();
if (super.addAll(c))
{
for (Song song : c)
{
if (this.shuffle)
{
newIndex = random.nextInt(this.queueOrder.size());
if (newIndex <= this.currentIndex)
{
this.currentIndex++;
}
this.queueOrder.add(newIndex, song);
}
else
{
this.queueOrder.add(song);
}
}
this.triggerQueueChangeListeners();
return true;
}
return false;
}
@Override
public boolean addAll(int index, Collection<? extends Song> c)
{
int newIndex;
int i = 0;
if (super.addAll(index, c))
{
for (Song song : c)
{
if (this.shuffle)
{
newIndex = random.nextInt(this.queueOrder.size());
}
else
{
newIndex = index + i++;
}
if (newIndex <= this.currentIndex)
{
this.currentIndex++;
}
this.queueOrder.add(newIndex, song);
}
this.triggerQueueChangeListeners();
return true;
}
return false;
}
@Override
protected void removeRange(int fromIndex, int toIndex)
{
for (int i = fromIndex; i < toIndex; i++)
{
this.queueOrder.remove(this.get(i));
if (i <= this.currentIndex)
{
this.currentIndex--;
}
}
super.removeRange(fromIndex, toIndex);
this.triggerQueueChangeListeners();
}
@Override
public boolean removeAll(Collection<?> c)
{
boolean success;
for (int i = 0, removed = 0; i <= this.currentIndex && i < this.size(); i++)
{
if (c.contains(this.get(i)))
{
this.currentIndex--;
}
}
this.queueOrder.removeAll(c);
success = super.removeAll(c);
this.triggerQueueChangeListeners();
return success;
}
@Override
public boolean retainAll(Collection<?> c)
{
boolean success;
for (int i = 0, removed = 0; i <= this.currentIndex && i < this.size(); i++)
{
if (!c.contains(this.get(i)))
{
this.currentIndex--;
}
}
this.queueOrder.retainAll(c);
success = super.retainAll(c);
this.triggerQueueChangeListeners();
return success;
}
/**
* Adds a listener for when the current song changes.
*
* @param listener - The listener to add.
*/
public void addSongChangeListener(SongChangeListener listener)
{
this.songListeners.add(listener);
}
/**
* Removes a listener for when the current song changes.
*
* @param listener - The listener to remove.
*/
public void removeSongChangeListener(SongChangeListener listener)
{
this.songListeners.remove(listener);
}
/**
* Tells all listeners that the song changed.
*/
protected void triggerSongChangeListeners()
{
this.songListeners.forEach(listener -> listener.onSongChange(this));
}
/**
* Adds a listener for when the queue contents changes.
*
* @param listener - The listener to add.
*/
public void addQueueChangeListener(QueueChangeListener listener)
{
this.queueListeners.add(listener);
}
/**
* Removes a listener for when the queue contents changes.
*
* @param listener - The listener to remove.
*/
public void removeQueueChangeListener(QueueChangeListener listener)
{
this.queueListeners.remove(listener);
}
/**
* Tells all listeners that the queue changed.
*/
protected void triggerQueueChangeListeners()
{
this.queueListeners.forEach(listener -> listener.onQueueChange(this));
}
public interface SongChangeListener extends EventListener
{
/**
* Called when the song of a queue changes.
*
* @param queue - The queue whose song changed.
*/
void onSongChange(Queue queue);
}
public interface QueueChangeListener extends EventListener
{
/**
* Called when the queue contents change.
*
* @param queue - The queue that changed.
*/
void onQueueChange(Queue queue);
}
}