Merges the two album tables into one.

Albums are independent of how the song is stored. We don't need to separate them.
This commit is contained in:
Markil3
2021-09-18 15:00:12 -06:00
parent 8bb7086a44
commit 2e2748f9e9
26 changed files with 2553 additions and 2576 deletions

View File

@@ -45,10 +45,10 @@ public class Main
logger.debug("Interface socket closed, shutting down"); logger.debug("Interface socket closed, shutting down");
return true; return true;
} }
/** /*
* Make sure that it is active. * Make sure that it is active.
*/ */
this.sendUpdate("ping"); // this.sendUpdate("ping");
return false; return false;
} }

View File

@@ -6,6 +6,15 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.File; import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Optional;
import java.util.Properties;
public class ConfigManager public class ConfigManager
{ {
@@ -23,6 +32,13 @@ public class ConfigManager
private static File appDir; private static File appDir;
private static File firefoxDir; private static File firefoxDir;
private static final File propsFile = new File(getConfigDir(), "config" +
".prop");
private static Properties props;
private static Path[] musicDirs;
private static Path[] musicIgnoreDirs;
/** /**
* Obtains the install directory for the application. * Obtains the install directory for the application.
* *
@@ -53,7 +69,8 @@ public class ConfigManager
} }
else else
{ {
firefoxDir = new File(firefoxDir.getParentFile().getParent(), firefoxDir = new File(firefoxDir.getParentFile()
.getParent(),
"firefox"); "firefox");
if (firefoxDir.exists()) if (firefoxDir.exists())
{ {
@@ -155,4 +172,141 @@ public class ConfigManager
} }
return commDir; return commDir;
} }
/**
* Gets the properties file
*
* @return The settings contained within the properties object.
*/
public static Properties getProperties()
{
if (props == null)
{
Properties defaultProps = new Properties();
defaultProps.setProperty("musicInclude", System.getProperty("user" +
".home") + File.separator + "Music" + File.pathSeparator + System
.getProperty("user.home") + File.separator + "My Music");
defaultProps.setProperty("musicExclude", "");
props = new Properties(defaultProps);
if (propsFile.exists())
{
loadProperties();
}
else
{
saveProperties();
}
}
return props;
}
public static void loadProperties()
{
try
{
props.load(new FileReader(propsFile));
}
catch (IOException e)
{
logger.error("Error reading configuration file", e);
}
}
public static void saveProperties()
{
try
{
props.store(new FileWriter(propsFile), "Universal Music " +
"Player Properties");
}
catch (IOException e)
{
logger.error("Error writing configuration file", e);
}
}
/**
* Obtains folders to scan for music.
*
* @return A list of folders to pay attention to.
*/
public static Path[] getMusicDirs()
{
if (musicDirs == null)
{
musicDirs =
Optional.ofNullable(getProperties().getProperty(
"musicInclude"))
.map(s -> s.split(File.pathSeparator)).stream()
.flatMap(Arrays::stream)
.map(String::trim).map(Paths::get)
.sorted(Path::compareTo)
.toArray(Path[]::new);
}
return musicDirs;
}
/**
* Obtains folders to ignore when scanning for music.
*
* @return A list of folders to ignore.
*/
public static Path[] getMusicExcludeDirs()
{
if (musicIgnoreDirs == null)
{
musicIgnoreDirs =
Optional.ofNullable(getProperties().getProperty(
"musicExclude"))
.map(s -> s.split(File.pathSeparator)).stream()
.flatMap(Arrays::stream)
.map(String::trim).map(Paths::get)
.sorted(Path::compareTo)
.toArray(Path[]::new);
}
return musicIgnoreDirs;
}
/**
* Checks to see whether a file should be scanned.
*
* @param file - The file to scan.
* @return Whether or not a file is scanned.
*/
public static boolean scanFolder(Path file)
{
for (Path musicIgnoreDir : musicIgnoreDirs)
{
if (file.startsWith(musicIgnoreDir) || musicIgnoreDir
.endsWith(file))
{
/*
* Make sure that we don't have an include path that takes
* precedence. Working backwards from more specific paths is
* more likely to get our results faster.
*/
for (int j = musicDirs.length - 1; j >= 0; j--)
{
if (file.startsWith(musicDirs[j]) || musicDirs[j]
.endsWith(file))
{
return true;
}
}
return false;
}
}
/*
* Considering that there was nothing in the ignore list, look to
* make sure that we are allowed to scan it.
*/
for (Path musicDir : musicDirs)
{
if (file.startsWith(musicDir) || musicDir.endsWith(file))
{
return true;
}
}
return false;
}
} }

View File

@@ -60,4 +60,14 @@ public class PlaybackInfo implements Serializable
{ {
return this.status; return this.status;
} }
@Override
public String toString()
{
return "PlaybackInfo{" +
"playTime=" + playTime +
", status=" + status +
", currentSong=" + currentSong +
'}';
}
} }

View File

@@ -0,0 +1,73 @@
/*
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
*/
package edu.regis.universeplayer.browserCommands;
import java.net.URL;
import edu.regis.universeplayer.data.InternetSong;
/**
* Asks the browser for information on a certain song.
*
* @author William Hubbard
* @version 0.2
*/
public class QuerySongData implements BrowserQuery<InternetSong>
{
/**
* The song to get data for.
*/
private URL url;
/**
* For serialization only. Do not use.
*/
public QuerySongData()
{
}
/**
* Creates a song data request.
*
* @param url - The URL to request data for.
*/
public QuerySongData(URL url)
{
this.url = url;
}
/**
* Obtains the name of the command.
*
* @return The command name.
*/
@Override
public String getCommandName()
{
return "getSongData";
}
/**
* Obtains the type of value this command returns.
*
* @return The return type.
*/
@Override
public Class<InternetSong> getReturnType()
{
return InternetSong.class;
}
/**
* Gets the location of the song to query.
*
* @return The song location.
*/
public URL getUrl()
{
return this.url;
}
}

View File

@@ -23,14 +23,28 @@ public class Album implements Comparable<Album>
public int compareTo(Album o) public int compareTo(Album o)
{ {
if (o != null && o.name != null) if (o != null && o.name != null)
{
if (this.name == null)
{
return 1;
}
else
{ {
return this.name.compareToIgnoreCase(o.name); return this.name.compareToIgnoreCase(o.name);
} }
}
else
{
if (this.name == null)
{
return 0;
}
else else
{ {
return -1; return -1;
} }
} }
}
@Override @Override
public String toString() public String toString()

View File

@@ -15,6 +15,10 @@ public class InternetSong extends Song
{ {
private static final Logger logger = LoggerFactory private static final Logger logger = LoggerFactory
.getLogger(InternetSong.class); .getLogger(InternetSong.class);
/**
* The location of the song.
*/
public URL location; public URL location;
@Override @Override
@@ -23,7 +27,7 @@ public class InternetSong extends Song
int compare = super.compareTo(o); int compare = super.compareTo(o);
if (compare == 0) if (compare == 0)
{ {
if (o instanceof LocalSong) if (o instanceof InternetSong)
{ {
try try
{ {

View File

@@ -12,9 +12,23 @@ import java.util.Arrays;
*/ */
public class LocalSong extends Song public class LocalSong extends Song
{ {
/**
* The file the song is stored at.
*/
public File file; public File file;
/**
* The format type the song is stored in.
*/
public String type; public String type;
/**
* The encoding format the song is recorded in.
*/
public String codec; public String codec;
/**
* The last modification time of this song file, as returned by {@link
* File#lastModified()}.
*/
public long lastMod;
@Override @Override
public int compareTo(Song o) public int compareTo(Song o)

View File

@@ -12,10 +12,29 @@ import java.util.Arrays;
*/ */
public class Song implements Comparable<Song>, Serializable public class Song implements Comparable<Song>, Serializable
{ {
/**
* The internal ID representing this song in the database.
*/
public int id;
/**
* The name of the song.
*/
public String title; public String title;
/**
* Artists who contributed to the song.
*/
public String[] artists; public String[] artists;
/**
* Which track number in the album the song belongs to.
*/
public int trackNum; public int trackNum;
/**
* Which disc
*/
public int disc; public int disc;
/**
* How long the song is in milliseconds.
*/
public long duration; public long duration;
/** /**

View File

@@ -23,6 +23,11 @@ import java.util.stream.Collectors;
import javax.swing.JOptionPane; import javax.swing.JOptionPane;
import edu.regis.universeplayer.browserCommands.QueryFuture; import edu.regis.universeplayer.browserCommands.QueryFuture;
import edu.regis.universeplayer.data.AlbumProvider;
import edu.regis.universeplayer.data.CompiledSongProvider;
import edu.regis.universeplayer.data.DefaultAlbumProvider;
import edu.regis.universeplayer.data.InternetSongProvider;
import edu.regis.universeplayer.data.LocalSongProvider;
import edu.regis.universeplayer.data.Queue; import edu.regis.universeplayer.data.Queue;
import edu.regis.universeplayer.data.Song; import edu.regis.universeplayer.data.Song;
import edu.regis.universeplayer.data.SongProvider; import edu.regis.universeplayer.data.SongProvider;
@@ -42,6 +47,19 @@ public class PlayerEnvironment
private static final ResourceBundle langs = ResourceBundle private static final ResourceBundle langs = ResourceBundle
.getBundle("lang.interface", Locale.getDefault()); .getBundle("lang.interface", Locale.getDefault());
private static AlbumProvider ALBUMS_INSTANCE;
private static SongProvider<?> SONGS_INSTANCE;
public static AlbumProvider getAlbums()
{
return ALBUMS_INSTANCE;
}
public static SongProvider<?> getSongs()
{
return SONGS_INSTANCE;
}
/** /**
* Initializes the various components, setting up listeners as needed. * Initializes the various components, setting up listeners as needed.
*/ */
@@ -124,6 +142,11 @@ public class PlayerEnvironment
InstanceConnector connector = new InstanceConnector(); InstanceConnector connector = new InstanceConnector();
new Thread(connector).start(); new Thread(connector).start();
ALBUMS_INSTANCE = new DefaultAlbumProvider();
SONGS_INSTANCE =
new CompiledSongProvider(new LocalSongProvider(ALBUMS_INSTANCE),
new InternetSongProvider(ALBUMS_INSTANCE));
Queue queue = Queue.getInstance(); Queue queue = Queue.getInstance();
PlayerManager playback = PlayerManager.getPlayers(); PlayerManager playback = PlayerManager.getPlayers();
queue.addSongChangeListener(queue1 -> { queue.addSongChangeListener(queue1 -> {
@@ -185,9 +208,10 @@ public class PlayerEnvironment
} }
}); });
playback.addPlaybackListener(status -> { playback.addPlaybackListener(status -> {
switch (status.getInfo().getStatus()) logger.info("Receiving {} from {}", status.getInfo(), status.getSource());
if (status.getInfo().getStatus() == PlaybackStatus.FINISHED)
{ {
case FINISHED -> Queue.getInstance().skipNext(); Queue.getInstance().skipNext();
} }
}); });
@@ -195,7 +219,7 @@ public class PlayerEnvironment
{ {
Interface inter = new Interface(); Interface inter = new Interface();
inter.setSize(700, 500); inter.setSize(700, 500);
SongProvider.INSTANCE.addUpdateListener(inter); SONGS_INSTANCE.addUpdateListener(inter);
inter.setVisible(true); inter.setVisible(true);
} }
@@ -222,7 +246,7 @@ public class PlayerEnvironment
Map<String, Object> options, Map<String, Object> options,
List<String> params) List<String> params)
{ {
int equals = -1; int equals;
Object value; Object value;
String key; String key;
String valStr; String valStr;
@@ -424,7 +448,7 @@ public class PlayerEnvironment
for (String param : params) for (String param : params)
{ {
String finalParam = param.toLowerCase(); String finalParam = param.toLowerCase();
SongProvider.INSTANCE.getSongs().forEach(song -> { getSongs().getSongs().forEach(song -> {
Integer matches = matchMap.get(song); Integer matches = matchMap.get(song);
if (matches == null) if (matches == null)
{ {

View File

@@ -0,0 +1,77 @@
package edu.regis.universeplayer.data;
import java.util.Collection;
import java.util.concurrent.Future;
/**
* Manages all albums that are part of the collection.
*/
public interface AlbumProvider extends DataProvider<Album>
{
/**
* Obtains all albums within the collection.
*
* @return A list of albums.
*/
Collection<Album> getAlbums();
/**
* Obtains a list of all album artists.
*
* @return All album artists.
*/
Collection<String> getAlbumArtists();
/**
* Obtains a list of all genres.
*
* @return All genres.
*/
Collection<String> getGenres();
/**
* Obtains a list of all years that have albums.
*
* @return All years.
*/
Collection<Integer> getYears();
/**
* Obtains an album by a specific name.
*
* @param name - The name to search for.
* @return - The first album that matches the given name, or null if that album name is not in
* the database.
*/
Album getAlbumByName(String name);
/**
* Obtains all albums that were written by a certain artist.
*
* @param artist - The artist to search for.
* @return - The collection on matching albums.
*/
Collection<Album> getAlbumsFromArtist(String artist);
/**
* Obtains all albums that match a certain genre
*
* @param genre - The genre to search for.
* @return - The collection on matching albums.
*/
Collection<Album> getAlbumsFromGenre(String genre);
/**
* Obtains all albums that were released a certain year.
*
* @param year - The year to search for.
* @return - The collection on matching albums.
*/
Collection<Album> getAlbumsFromYear(int year);
/**
* Writes an album to the collection.
* @param album - The album to add.
*/
Future<Album> writeItem(Album album);
}

View File

@@ -5,6 +5,7 @@
package edu.regis.universeplayer.data; package edu.regis.universeplayer.data;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
/** /**
* A song provider that serves as a central point for any and all song * A song provider that serves as a central point for any and all song
@@ -13,61 +14,25 @@ import java.util.*;
* @author William Hubbard * @author William Hubbard
* @version 0.1 * @version 0.1
*/ */
public class CompiledSongProvider implements SongProvider<Song> public class CompiledSongProvider implements SongProvider<Song>, UpdateListener
{ {
private final LinkedList<UpdateListener> listeners = new LinkedList<>(); private final LinkedList<UpdateListener> listeners = new LinkedList<>();
/** /**
* A set of all providers we pull from * A set of all providers we pull from
*/ */
private final HashMap<SongProvider<?>, Set<Song>> providers = new HashMap<>(); private final HashSet<SongProvider<? extends Song>> providers =
new HashSet<>();
private AlbumProvider albums;
/** /**
* The collection of update listeners for each provider. * Creates a new CompiledSongProvider containing a set of existing
*/ * providers.
private final HashMap<SongProvider<?>, UpdateListener> updateListener = new HashMap<>();
/**
* A cache of all albums used.
*/
private final HashMap<Album, Set<Song>> cachedAlbums = new HashMap<>();
/**
* A cache of all album names.
*/
private final HashMap<String, Album> cachedAlbumNames = new HashMap<>();
/**
* A cache of all songs used.
*/
private final HashSet<Song> cachedSongs = new HashSet<>();
/**
* A cache of all song artists used.
*/
private final HashMap<String, Set<Song>> cachedArtists = new HashMap<>();
/**
* A cache of all album artists used.
*/
private final HashMap<String, Set<Album>> cachedAlbumArtists = new HashMap<>();
/**
* A cache of all genres used.
*/
private final HashMap<String, Set<Album>> cachedGenres = new HashMap<>();
/**
* A cache of all years used.
*/
private final HashMap<Integer, Set<Album>> cachedYears = new HashMap<>();
/**
* Creates a new CompiledSongProvider containing a set of existing providers.
* *
* @param providers - Providers to add. * @param providers - Providers to add.
*/ */
public CompiledSongProvider(SongProvider<?>... providers) public CompiledSongProvider(SongProvider<?
>... providers)
{ {
for (SongProvider<?> provider : providers) for (SongProvider<?> provider : providers)
{ {
@@ -80,83 +45,17 @@ public class CompiledSongProvider implements SongProvider<Song>
* *
* @param provider - The provider to add. * @param provider - The provider to add.
*/ */
public void addProvider(SongProvider<?> provider) public <T extends Song> void addProvider(SongProvider<T> provider)
{ {
UpdateListener listener; UpdateListener listener;
if (!this.providers.containsKey(provider)) if (this.providers.add(provider))
{ {
this.providers.put(provider, new HashSet<>(provider.getSongs())); if (this.albums == null)
listener = (song, totalSongs, updateText) -> {
/*
* Resets the cache.
*/
if (song == totalSongs || totalSongs == 0)
{ {
this.removeFromCache(this.providers.get(provider)); this.albums = provider.getAlbumProvider();
this.addToCache(provider);
}
this.triggerUpdateListeners();
};
provider.addUpdateListener(listener);
this.addToCache(provider);
}
}
/**
* Caches all the songs contained in a provider.
*
* @param provider - The provider to cache.
*/
private <T extends Song> void addToCache(SongProvider<T> provider)
{
for (T song : provider.getSongs())
{
if (this.cachedSongs.add(song))
{
this.cachedAlbumNames.put(song.album.name, song.album);
if (!this.cachedAlbums.containsKey(song.album))
{
this.cachedAlbums.put(song.album, new HashSet<>());
}
this.cachedAlbums.get(song.album).add(song);
for (String artist : song.artists)
{
if (!this.cachedArtists.containsKey(artist))
{
this.cachedArtists.put(artist, new HashSet<>());
}
this.cachedArtists.get(artist).add(song);
}
for (String artist : song.album.artists)
{
if (!this.cachedAlbumArtists.containsKey(artist))
{
this.cachedAlbumArtists.put(artist, new HashSet<>());
}
this.cachedAlbumArtists.get(artist).add(song.album);
}
for (String genre : song.album.genres)
{
if (!this.cachedGenres.containsKey(genre))
{
this.cachedGenres.put(genre, new HashSet<>());
}
this.cachedGenres.get(genre).add(song.album);
}
if (!this.cachedYears.containsKey(song.album.year))
{
this.cachedYears.put(song.album.year, new HashSet<>());
}
this.cachedYears.get(song.album.year).add(song.album);
}
else
{
/*
* If we couldn't add it, then another provider has provided that song already. We
* should remove it from our collection just to ensure that there is no confusion.
*/
this.providers.get(provider).remove(song);
} }
provider.addUpdateListener(this);
triggerUpdateListeners();
} }
} }
@@ -167,73 +66,36 @@ public class CompiledSongProvider implements SongProvider<Song>
*/ */
public void removeProvider(SongProvider<?> provider) public void removeProvider(SongProvider<?> provider)
{ {
this.removeFromCache(this.providers.remove(provider)); provider.removeUpdateListener(this);
provider.removeUpdateListener(this.updateListener.remove(provider)); this.providers.remove(provider);
} }
/** @Override
* Removes all songs from a provider from a cache. public AlbumProvider getAlbumProvider()
*
* @param songs - The songs to move out.
*/
private void removeFromCache(Set<Song> songs)
{ {
if (songs != null) return this.albums;
{
for (Song song : songs)
{
this.cachedSongs.remove(song);
this.cachedAlbums.get(song.album).remove(song);
/*
* Remove empty albums
*/
if (this.cachedAlbums.get(song.album).isEmpty())
{
this.cachedAlbums.remove(song.album);
this.cachedAlbumNames.remove(song.album.name);
} }
for (String artist : song.artists)
@Override
public void joinUpdate() throws InterruptedException
{ {
this.cachedArtists.get(artist).remove(song); for (SongProvider provider: this.providers)
if (this.cachedArtists.get(artist).isEmpty())
{ {
this.cachedArtists.remove(artist); provider.joinUpdate();
}
}
for (String artist : song.album.artists)
{
this.cachedAlbumArtists.get(artist).remove(song.album);
if (this.cachedAlbumArtists.get(artist).isEmpty())
{
this.cachedAlbumArtists.remove(artist);
}
}
for (String genre : song.album.genres)
{
this.cachedGenres.get(genre).remove(song.album);
if (this.cachedGenres.get(genre).isEmpty())
{
this.cachedGenres.remove(genre);
}
}
this.cachedYears.get(song.album.year).remove(song.album);
if (this.cachedYears.get(song.album.year).isEmpty())
{
this.cachedYears.remove(song.album.year);
}
}
} }
} }
/** /**
* Obtains all albums within the collection. * Obtains the collection of items.
* *
* @return A list of albums. * @return A collection of items parsed from the database.
*/ */
@Override @Override
public Collection<Album> getAlbums() public Set<Song> getCollection()
{ {
return this.cachedAlbums.keySet(); return this.providers.stream().map(SongProvider::getCollection)
.flatMap(Collection::stream)
.collect(Collectors.toSet());
} }
/** /**
@@ -244,7 +106,7 @@ public class CompiledSongProvider implements SongProvider<Song>
@Override @Override
public Collection<Song> getSongs() public Collection<Song> getSongs()
{ {
return this.cachedSongs; return this.getCollection();
} }
/** /**
@@ -255,121 +117,48 @@ public class CompiledSongProvider implements SongProvider<Song>
@Override @Override
public Collection<String> getArtists() public Collection<String> getArtists()
{ {
return this.cachedArtists.keySet(); return this.providers.stream().map(SongProvider::getArtists)
} .flatMap(Collection::stream)
.collect(Collectors.toSet());
/**
* Obtains a list of all album artists.
*
* @return All album artists.
*/
@Override
public Collection<String> getAlbumArtists()
{
return this.cachedAlbumArtists.keySet();
}
/**
* Obtains a list of all genres.
*
* @return All genres.
*/
@Override
public Collection<String> getGenres()
{
return this.cachedGenres.keySet();
}
/**
* Obtains a list of all years that have albums.
*
* @return All years.
*/
@Override
public Collection<Integer> getYears()
{
return this.cachedYears.keySet();
} }
/** /**
* Obtains all songs from an album. * Obtains all songs from an album.
* *
* @param album - The album to obtain * @param album - The album to obtain
* @return All songs from the requested album, or null if that album is not in the database. * @return All songs from the requested album, or null if that album is not
* in the database.
*/ */
@Override @Override
public Collection<Song> getSongsFromAlbum(Album album) public Collection<Song> getSongsFromAlbum(Album album)
{ {
return this.cachedAlbums.get(album); return this.providers.stream()
.map(p -> p.getSongsFromAlbum(album))
.flatMap(Collection::stream)
.collect(Collectors.toSet());
} }
/** /**
* Obtains all songs written by a given artist. * Obtains all songs written by a given artist.
* *
* @param artist - The artist to search for * @param artist - The artist to search for
* @return A list of all songs from the specified artist, or null if that artist is not in the * @return A list of all songs from the specified artist, or null if that
* database. * artist is not in the database.
*/ */
@Override @Override
public Collection<Song> getSongsFromArtist(String artist) public Collection<Song> getSongsFromArtist(String artist)
{ {
return this.cachedArtists.get(artist); return this.providers.stream()
} .map(p -> p.getSongsFromArtist(artist))
.flatMap(Collection::stream)
/** .collect(Collectors.toSet());
* Obtains an album by a specific name.
*
* @param name - The name to search for.
* @return - The first album that matches the given name, or null if that album name is not in
* the database.
*/
@Override
public Album getAlbumByName(String name)
{
return this.cachedAlbumNames.get(name);
}
/**
* Obtains all albums that were written by a certain artist.
*
* @param artist - The artist to search for.
* @return - The collection on matching albums.
*/
@Override
public Collection<Album> getAlbumsFromArtist(String artist)
{
return this.cachedAlbumArtists.get(artist);
}
/**
* Obtains all albums that match a certain genre
*
* @param genre - The genre to search for.
* @return - The collection on matching albums.
*/
@Override
public Collection<Album> getAlbumsFromGenre(String genre)
{
return this.cachedGenres.get(genre);
}
/**
* Obtains all albums that were released a certain year.
*
* @param year - The year to search for.
* @return - The collection on matching albums.
*/
@Override
public Collection<Album> getAlbumsFromYear(int year)
{
return this.cachedYears.get(year);
} }
@Override @Override
public int getUpdateProgress() public int getUpdateProgress()
{ {
int totalUpdate = 0; int totalUpdate = 0;
for (SongProvider<?> provider : this.providers.keySet()) for (SongProvider<?> provider : this.providers)
{ {
totalUpdate += provider.getUpdateProgress(); totalUpdate += provider.getUpdateProgress();
} }
@@ -377,18 +166,18 @@ public class CompiledSongProvider implements SongProvider<Song>
} }
@Override @Override
public int getTotalUpdateSongs() public int getTotalUpdates()
{ {
int totalUpdate = 0; int totalUpdate = 0;
for (SongProvider<?> provider : this.providers.keySet()) for (SongProvider<?> provider : this.providers)
{ {
if (provider.getTotalUpdateSongs() == -1) if (provider.getTotalUpdates() == -1)
{ {
return -1; return -1;
} }
else else
{ {
totalUpdate += provider.getTotalUpdateSongs(); totalUpdate += provider.getTotalUpdates();
} }
} }
return totalUpdate; return totalUpdate;
@@ -397,7 +186,7 @@ public class CompiledSongProvider implements SongProvider<Song>
@Override @Override
public String getUpdateText() public String getUpdateText()
{ {
for (SongProvider<?> provider: this.providers.keySet()) for (SongProvider<?> provider : this.providers)
{ {
if (provider.getUpdateText() != null) if (provider.getUpdateText() != null)
{ {
@@ -426,7 +215,23 @@ public class CompiledSongProvider implements SongProvider<Song>
{ {
for (UpdateListener listener : this.listeners) for (UpdateListener listener : this.listeners)
{ {
listener.onUpdate(this.getUpdateProgress(), this.getTotalUpdateSongs(), this.getUpdateText()); listener.onUpdate(this, this.getUpdateProgress(),
this.getTotalUpdates(), this.getUpdateText());
} }
} }
/**
* Called when the update status of the player has changed.
*
* @param provider - The provider that triggered the listener.
* @param updated - The number of songs updated.
* @param totalUpdate - The total number of songs to update, or -1 if we are
* still determining that.
* @param updating - The text to display on update bars.
*/
@Override
public <T> void onUpdate(DataProvider<T> provider, int updated, int totalUpdate, String updating)
{
this.triggerUpdateListeners();
}
} }

View File

@@ -0,0 +1,73 @@
package edu.regis.universeplayer.data;
import java.util.Set;
/**
* A data provider serves as a place to get songs, albums, or any other
* collection of information needed by the music player.
*/
public interface DataProvider<T>
{
/**
* Checks to see if we are updating any songs.
*
* @return True if we are updating the song cache.
*/
default boolean isUpdating()
{
return this.getTotalUpdates() != 0;
}
/**
* If there is an update in progress, this halts the calling thread until
* the update is complete.
*/
void joinUpdate() throws InterruptedException;
/**
* When we are updating the cache, this method obtains the number of items
* already updated.
*
* @return The number of items successfully updated.
*/
int getUpdateProgress();
/**
* Determines whether or not we are updating the cache and, if so, gets the
* total number of items we need to update
*
* @return The total number of items to update. A 0 means that we do not
* have any items to update, and a negative number means that we are
* currently calculating how many items we need to update.
*/
int getTotalUpdates();
/**
* Determines the text displayed for the progress of updates
*
* @return The update status text.
*/
String getUpdateText();
/**
* Obtains the collection of items.
*
* @return A collection of items parsed from the database.
*/
Set<T> getCollection();
/**
* Adds a listener for song updates.
*
* @param listener - The listener to add.
*/
void addUpdateListener(UpdateListener listener);
/**
* Removes a listener for song updates.
*
* @param listener - The listener to remove.
*/
void removeUpdateListener(UpdateListener listener);
}

View File

@@ -0,0 +1,488 @@
/*
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
*/
package edu.regis.universeplayer.data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Formatter;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveAction;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
/**
* A default data provider that pulls information from a database.
*/
public abstract class DatabaseProvider<T> implements DataProvider<T>
{
private static final Logger logger =
LoggerFactory.getLogger(DatabaseProvider.class);
private static final ResourceBundle langs = ResourceBundle
.getBundle("lang.interface", Locale.getDefault());
protected final ForkJoinPool service = new ForkJoinPool();
private final LinkedList<UpdateListener> listeners = new LinkedList<>();
private final AtomicInteger progress = new AtomicInteger(0);
private final AtomicInteger updating = new AtomicInteger(0);
private String updateItem;
private final HashSet<T> collection = new HashSet<>();
public DatabaseProvider()
{
updateCache();
}
/**
* Searches the database and updates the collection from there.
*/
public final void updateCache()
{
updating.set(-1);
service.submit(new DatabaseProvider.SongQuery(true));
}
@Override
public final void joinUpdate() throws InterruptedException
{
synchronized (this.updating)
{
if (this.isUpdating())
{
this.updating.wait();
}
}
}
@Override
public int getUpdateProgress()
{
return this.progress.get();
}
@Override
public int getTotalUpdates()
{
return this.updating.get();
}
@Override
public String getUpdateText()
{
synchronized (this.collection)
{
if (this.updateItem != null)
{
return new Formatter().format(langs.getString("update" +
".database"), this.updateItem).toString();
}
else
{
return null;
}
}
}
/**
* {@inheritDoc}
* <p>
* Note that listeners are triggered in the same thread that handles
* updates, which does block the updates. It is recommended to manually
* redirect the event to another thread.
* </p>
*
* @param listener - The listener to add.
*/
@Override
public void addUpdateListener(UpdateListener listener)
{
this.listeners.add(listener);
}
@Override
public void removeUpdateListener(UpdateListener listener)
{
this.listeners.remove(listener);
}
protected void triggerUpdateListeners()
{
this.listeners.forEach(listener -> listener
.onUpdate(this, this.getUpdateProgress(), this
.getTotalUpdates(), this.getUpdateText()));
}
/**
* Obtains the name of
*
* @return The name of the database table. This is case insensitive.
*/
protected abstract String getDatabaseTable();
/**
* Obtains the SQL string used to create the database table should it be
* necessary.
*
* @return The SQL command that creates the database table. It should take
* the format of "CREATE TABLE name (param1 type, param2 type);"
*/
protected abstract String createDatabaseTable();
/**
* Called when an entry is read from the database and is ready to be
* parsed.
* <p>
* Note that this method is called for every row. Do NOT call {@link
* ResultSet#next()}!
*
* @param result The result that is read from.
*/
protected abstract T readResult(ResultSet result) throws SQLException;
/**
* Called to obtain the properties of an object to write.
*
* @param item - The item to serialize.
* @return Properties to write.
*/
protected abstract Map<String, Object> serializeItem(T item);
/**
* Adds an item to the database.
*
* @param item - The item to write.
*/
public final Future<T> writeItem(T item)
{
synchronized (this.collection)
{
// logger.debug("Writing {}", item);
this.collection.add(item);
}
WriterAction action = new WriterAction(item);
service.execute(action);
return action;
}
/**
* Converts a piece of data into a string that will display in the update
* text.
*
* @param data - The data to stringify.
* @return A string representation of the data being updated.
*/
protected abstract String stringifyResult(T data);
/**
* A callback for when the database scan is complete.
* <p>
* Note that this method is called from the same thread that the scanner is
* from.
* </p>
*
* @return A fork-join task to invoke. This may be null.
*/
protected abstract ForkJoinTask[] onComplete();
private void createDatabaseTable(Statement state, String table) throws SQLException
{
String rawStatement;
logger.debug("Creating {} table.", table);
/*
* Create the table
*/
rawStatement = createDatabaseTable();
if (!Pattern
.compile("^\\s*CREATE\\s+TABLE\\s*" + table + "\\s*\\(" +
"(\\w+\\s+[a-zA-Z0-9()]+(\\s+\\w+)*\\s*)(,\\s*\\w+\\s+[a-zA-Z0-9()]+(\\s+\\w+)*\\s*)*\\);$",
Pattern.CASE_INSENSITIVE).matcher(
rawStatement).matches())
{
throw new IllegalArgumentException("Invalid " +
"table creation statement: " +
"\"" + rawStatement +
"\"");
}
state.executeUpdate(rawStatement);
}
/**
* Obtains the collection of items.
*
* @return A collection of items parsed from the database.
*/
@Override
public final Set<T> getCollection()
{
synchronized (this.collection)
{
return new HashSet<>(this.collection);
}
}
private class WriterAction extends ForkJoinTask<T>
{
private T item;
private Map<String, Object> string;
public WriterAction(T item)
{
this.item = item;
}
/**
* Returns the result that would be returned by {@link #join}, even if
* this task completed abnormally, or {@code null} if this task is not
* known to have been completed. This method is designed to aid
* debugging, as well as to support extensions. Its use in any other
* context is discouraged.
*
* @return the result, or {@code null} if not completed
*/
@Override
public T getRawResult()
{
return item;
}
/**
* Forces the given value to be returned as a result. This method is
* designed to support extensions, and should not in general be called
* otherwise.
*
* @param value the value
*/
@Override
protected void setRawResult(T value)
{
this.item = value;
}
/**
* Immediately performs the base action of this task and returns true
* if, upon return from this method, this task is guaranteed to have
* completed. This method may return false otherwise, to indicate that
* this task is not necessarily complete (or is not known to be
* complete), for example in asynchronous actions that require explicit
* invocations of completion methods. This method may also throw an
* (unchecked) exception to indicate abnormal exit. This method is
* designed to support extensions, and should not in general be called
* otherwise.
*
* @return {@code true} if this task is known to have completed normally
*/
@Override
protected boolean exec()
{
ResultSet result;
Statement state;
String table = getDatabaseTable();
this.string = serializeItem(this.item);
String index =
this.string.keySet().stream().findFirst().orElse(null);
Object indexValue = this.string.get(index);
synchronized (DatabaseManager.getDb())
{
try
{
state = DatabaseManager.getDb().createStatement();
result = state
.executeQuery("SELECT name FROM sqlite_master" +
" WHERE type='table' AND name='" + table + "';");
if (!result.next())
{
createDatabaseTable(state, table);
}
PreparedStatement prepState = DatabaseManager.getDb()
.prepareStatement(
"SELECT * FROM " + table + " WHERE " + index + " " +
"= ?");
prepState.setObject(1, indexValue);
result = prepState.executeQuery();
if (result.next())
{
logger.debug("Updating {}", this.string);
this.string.forEach((key, value) -> {
try
{
PreparedStatement prepState1 =
DatabaseManager.getDb()
.prepareStatement(
"UPDATE " + table + " SET " + key +
" = ? WHERE " +
index + " = ?");
prepState1.setObject(1, indexValue);
prepState1.setObject(2, value);
}
catch (SQLException throwables)
{
logger.error("Could not update " + key, throwables);
}
});
}
else
{
logger.debug("Inserting {}", this.string);
AtomicInteger i = new AtomicInteger(1);
PreparedStatement finalPrepState = DatabaseManager
.getDb().prepareStatement(
"INSERT INTO " + table + " VALUES (?" + ", ?"
.repeat(this.string
.size() - 1) +
")");
this.string.forEach((key, value) -> {
try
{
finalPrepState
.setObject(i.getAndIncrement(), value);
}
catch (SQLException throwables)
{
logger.error("Could not update " + key, throwables);
}
});
int count = finalPrepState.executeUpdate();
if (count == 0)
{
logger.error("Failed to insert {}", this.string);
}
}
}
catch (Exception e)
{
logger.error("Could not write object {}", this.string, e);
}
}
return true;
}
}
/**
* Scans the database for information
*/
private class SongQuery extends ForkJoinTask<Void>
{
private final boolean scan;
SongQuery(boolean scan)
{
this.scan = scan;
}
@Override
public Void getRawResult()
{
return null;
}
@Override
protected void setRawResult(Void value)
{
}
@Override
protected boolean exec()
{
Statement state;
String table = getDatabaseTable();
ResultSet result;
T item;
synchronized (updating)
{
updating.set(-1);
try
{
logger.debug("Querying database.");
/*
* Check if the table exists
*/
synchronized (DatabaseManager.getDb())
{
state = DatabaseManager.getDb().createStatement();
result = state
.executeQuery("SELECT name FROM sqlite_master" +
" WHERE type='table' AND name='" + table + "';");
if (!result.next())
{
createDatabaseTable(state, table);
}
else
{
result = state
.executeQuery("SELECT count(*) FROM " + table + ";");
updating.set(result.getInt(1));
triggerUpdateListeners();
result = state
.executeQuery("SELECT * FROM " + table + ";");
while (result.next())
{
item = readResult(result);
synchronized (collection)
{
updateItem = stringifyResult(item);
collection.add(item);
progress.incrementAndGet();
}
triggerUpdateListeners();
}
}
state.close();
}
}
catch (SQLException e)
{
logger.error("Could not query SQL database.", e);
}
finally
{
logger.debug("Query complete, retrieved {} items",
collection.size());
progress.set(0);
updating.set(0);
updateItem = null;
updating.notifyAll();
triggerUpdateListeners();
logger.debug("Searching for post-query tasks");
try
{
ForkJoinTask[] runners = onComplete();
if (runners != null)
{
logger.debug("Running {} post-query tasks", runners.length);
invokeAll(runners);
}
}
catch (Exception e)
{
logger.error("Could not get runners", e);
}
}
}
return true;
}
}
}

View File

@@ -0,0 +1,282 @@
package edu.regis.universeplayer.data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ForkJoinTask;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
public class DefaultAlbumProvider extends DatabaseProvider<Album> implements AlbumProvider
{
private static final Logger logger =
LoggerFactory.getLogger(DefaultAlbumProvider.class);
/**
* Obtains all albums within the collection.
*
* @return A list of albums.
*/
@Override
public Collection<Album> getAlbums()
{
return this.getCollection();
}
/**
* Obtains a list of all album artists.
*
* @return All album artists.
*/
@Override
public Collection<String> getAlbumArtists()
{
return this.getCollection().stream().filter(a -> a.artists != null)
.map(a -> new HashSet<>(Arrays.asList(a.artists)))
.reduce(new HashSet<>(), (strings, strings2) -> {
HashSet<String> comb = new HashSet<>();
comb.addAll(strings);
comb.addAll(strings2);
return comb;
});
}
/**
* Obtains a list of all genres.
*
* @return All genres.
*/
@Override
public Collection<String> getGenres()
{
return this.getCollection().stream().filter(a -> a.genres != null)
.map(a -> new HashSet<>(Arrays.asList(a.genres)))
.reduce(new HashSet<>(), (strings, strings2) -> {
HashSet<String> comb = new HashSet<>();
comb.addAll(strings);
comb.addAll(strings2);
return comb;
});
}
/**
* Obtains a list of all years that have albums.
*
* @return All years.
*/
@Override
public Collection<Integer> getYears()
{
return this.getCollection().stream().map(a -> a.year)
.collect(Collectors.toSet());
}
/**
* Obtains an album by a specific name.
*
* @param name - The name to search for.
* @return - The first album that matches the given name, or null if that
* album name is not in the database.
*/
@Override
public Album getAlbumByName(String name)
{
return this.getCollection().stream()
.filter(a -> a.name == null && name == null ||
a.name != null && a.name.equals(name))
.findFirst().orElse(null);
}
/**
* Obtains all albums that were written by a certain artist.
*
* @param artist - The artist to search for.
* @return - The collection on matching albums.
*/
@Override
public Collection<Album> getAlbumsFromArtist(String artist)
{
return this.getCollection().stream().filter(a -> {
if (a.artists != null)
{
for (int i = 0; i < a.artists.length; i++)
{
if (a.artists[i].equals(artist))
{
return true;
}
}
}
return false;
}).collect(Collectors.toSet());
}
/**
* Obtains all albums that match a certain genre
*
* @param genre - The genre to search for.
* @return - The collection on matching albums.
*/
@Override
public Collection<Album> getAlbumsFromGenre(String genre)
{
return this.getCollection().stream().filter(a -> {
if (a.genres != null)
{
for (int i = 0; i < a.genres.length; i++)
{
if (a.genres[i].equals(genre))
{
return true;
}
}
}
return false;
}).collect(Collectors.toSet());
}
/**
* Obtains all albums that were released a certain year.
*
* @param year - The year to search for.
* @return - The collection on matching albums.
*/
@Override
public Collection<Album> getAlbumsFromYear(int year)
{
return this.getCollection().stream().filter(a -> a.year == year)
.collect(Collectors.toSet());
}
/**
* Obtains the name of
*
* @return The name of the database table. This is case insensitive.
*/
@Override
protected String getDatabaseTable()
{
return "albums";
}
/**
* Obtains the SQL string used to create the database table should it be
* necessary.
*
* @return The SQL command that creates the database table. It should take
* the format of "CREATE TABLE name (param1 type, param2 type);"
*/
@Override
protected String createDatabaseTable()
{
return "CREATE TABLE albums (album TEXT PRIMARY KEY," +
"artists TEXT," +
"year INTEGER," +
"genres TEXT," +
"tracks INTEGER," +
"discs INTEGER);";
}
/**
* Called when an entry is read from the database and is ready to be
* parsed.
* <p>
* Note that this method is called for every row. Do NOT call {@link
* ResultSet#next()}!
*
* @param result The result that is read from.
*/
@Override
protected Album readResult(ResultSet result) throws SQLException
{
Album album = new Album();
album.id = result.getRow();
album.name = result.getString("album");
album.artists =
Optional.ofNullable(result.getString("artists"))
.map(s -> s.split(";")).stream()
.mapMulti((BiConsumer<String[], Consumer<String>>) (strings, objectConsumer) -> {
for (String string : strings)
{
if (!string.isEmpty())
{
objectConsumer.accept(string);
}
}
}).map(String::trim).toArray(String[]::new);
album.year = result.getInt("year");
album.genres =
Optional.ofNullable(result.getString("genres"))
.map(s -> s.split(";")).stream()
.mapMulti((BiConsumer<String[], Consumer<String>>) (strings, objectConsumer) -> {
for (String string : strings)
{
if (!string.isEmpty())
{
objectConsumer.accept(string);
}
}
}).map(String::trim).toArray(String[]::new);
album.totalTracks = result.getInt("tracks");
album.totalDiscs = result.getInt("discs");
return album;
}
/**
* Called to obtain the properties of an object to write.
*
* @param item - The item to serialize.
* @return Properties to write.
*/
@Override
protected Map<String, Object> serializeItem(Album item)
{
LinkedHashMap<String, Object> returnValue = new LinkedHashMap<>();
returnValue.put("album", item.name);
returnValue.put("artists", Arrays.stream(item.artists).reduce("",
(s1, s2) -> s1.isEmpty() ? s2 : s1 + ";" + s2));
returnValue.put("year", item.year);
returnValue.put("genres", Arrays.stream(item.genres).reduce("",
(s1, s2) -> s1.isEmpty() ? s2 : s1 + ";" + s2));
returnValue.put("tracks", item.totalTracks);
returnValue.put("discs", item.totalDiscs);
return returnValue;
}
/**
* Converts a piece of data into a string that will display in the update
* text.
*
* @param data - The data to stringify.
* @return A string representation of the data being updated.
*/
@Override
protected String stringifyResult(Album data)
{
return data.name;
}
/**
* A callback for when the database scan is complete.
* <p>
* Note that this method is called from the same thread that the scanner is
* from.
* </p>
*
* @return A fork-join task to invoke. This may be null.
*/
@Override
protected ForkJoinTask[] onComplete()
{
return null;
}
}

View File

@@ -7,657 +7,296 @@ package edu.regis.universeplayer.data;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Statement; import java.sql.Statement;
import java.util.*; import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class InternetSongProvider implements SongProvider<InternetSong> import edu.regis.universeplayer.browser.Browser;
import edu.regis.universeplayer.browserCommands.QuerySongData;
public class InternetSongProvider extends DatabaseProvider<InternetSong> implements SongProvider<InternetSong>
{ {
private static final Logger logger = LoggerFactory.getLogger(InternetSongProvider.class); private static final Logger logger = LoggerFactory.getLogger(InternetSongProvider.class);
private static final ExecutorService service = Executors.newSingleThreadExecutor();
private static InternetSongProvider INSTANCE; private static InternetSongProvider INSTANCE;
private final AtomicInteger progress = new AtomicInteger(0);
private final AtomicInteger updating = new AtomicInteger(0);
private String updateItem;
private final AlbumProvider albums;
public static InternetSongProvider getInstance() public static InternetSongProvider getInstance()
{ {
if (INSTANCE == null)
{
INSTANCE = new InternetSongProvider();
}
return INSTANCE; return INSTANCE;
} }
private AtomicBoolean updating = new AtomicBoolean();
private final HashMap<URL, InternetSong> songs = new HashMap<>();
private final HashMap<String, Album> albums = new HashMap<>();
/**
* A cache of all song artists.
*/
private final HashSet<String> artists = new HashSet<>();
/**
* A cache of all album genres.
*/
private final HashSet<String> genres = new HashSet<>();
/**
* A cache of all album artists.
*/
private final HashSet<String> albumArtists = new HashSet<>();
/**
* A cache of all album release years.
*/
private final HashSet<Integer> years = new HashSet<>();
private int updatedSongs;
private int totalUpdate;
private final LinkedList<UpdateListener> listeners = new LinkedList<>(); private final LinkedList<UpdateListener> listeners = new LinkedList<>();
private InternetSongProvider() public InternetSongProvider(AlbumProvider albums)
{ {
this.getSongCache(); this.albums = albums;
} INSTANCE = this;
private void getSongCache()
{
this.updating.set(true);
service.submit(new SongQuery());
}
/**
* Obtains all albums within the collection.
*
* @return A list of albums.
*/
@Override
public Collection<Album> getAlbums()
{
synchronized (this.updating)
{
while (this.updating.get())
{
try
{
this.updating.wait();
}
catch (InterruptedException e)
{
logger.error("Error while waiting for update lock", e);
}
}
}
return this.albums.values();
}
/**
* Obtains all songs within the collection.
*
* @return A list of songs.
*/
@Override
public Collection<InternetSong> getSongs()
{
synchronized (this.updating)
{
while (this.updating.get())
{
try
{
this.updating.wait();
}
catch (InterruptedException e)
{
logger.error("Error while waiting for update lock", e);
}
}
}
synchronized (this.songs)
{
return Collections.unmodifiableCollection(this.songs.values());
}
}
/**
* Obtains a list of all artists.
*
* @return All artists.
*/
@Override
public Collection<String> getArtists()
{
synchronized (this.updating)
{
while (this.updating.get())
{
try
{
this.updating.wait();
}
catch (InterruptedException e)
{
logger.error("Error while waiting for update lock", e);
}
}
}
return this.artists;
}
/**
* Obtains a list of all album artists.
*
* @return All album artists.
*/
@Override
public Collection<String> getAlbumArtists()
{
synchronized (this.updating)
{
while (this.updating.get())
{
try
{
this.updating.wait();
}
catch (InterruptedException e)
{
logger.error("Error while waiting for update lock", e);
}
}
}
return this.albumArtists;
}
/**
* Obtains a list of all genres.
*
* @return All genres.
*/
@Override
public Collection<String> getGenres()
{
synchronized (this.updating)
{
while (this.updating.get())
{
try
{
this.updating.wait();
}
catch (InterruptedException e)
{
logger.error("Error while waiting for update lock", e);
}
}
}
return this.genres;
}
/**
* Obtains a list of all years that have albums.
*
* @return All years.
*/
@Override
public Collection<Integer> getYears()
{
synchronized (this.updating)
{
while (this.updating.get())
{
try
{
this.updating.wait();
}
catch (InterruptedException e)
{
logger.error("Error while waiting for update lock", e);
}
}
}
return this.years;
}
/**
* Obtains all songs from an album.
*
* @param album - The album to obtain
* @return All songs from the requested album, or null if that album is not in the database.
*/
@Override
public Collection<InternetSong> getSongsFromAlbum(Album album)
{
synchronized (this.updating)
{
while (this.updating.get())
{
try
{
this.updating.wait();
}
catch (InterruptedException e)
{
logger.error("Error while waiting for update lock", e);
}
}
}
synchronized (this.songs)
{
return this.songs.values().stream().filter(song -> song.album.equals(album)).collect(Collectors.toUnmodifiableSet());
}
}
/**
* Obtains all songs written by a given artist.
*
* @param artist - The artist to search for
* @return A list of all songs from the specified artist, or null if that artist is not in the
* database.
*/
@Override
public Collection<InternetSong> getSongsFromArtist(String artist)
{
synchronized (this.updating)
{
while (this.updating.get())
{
try
{
this.updating.wait();
}
catch (InterruptedException e)
{
logger.error("Error while waiting for update lock", e);
}
}
}
synchronized (this.songs)
{
return this.songs.values().stream().filter(song -> Arrays.asList(song.artists).contains(artist)).collect(Collectors.toUnmodifiableSet());
}
}
/**
* Obtains an album by a specific name.
*
* @param name - The name to search for.
* @return - The first album that matches the given name, or null if that album name is not in
* the database.
*/
@Override
public Album getAlbumByName(String name)
{
synchronized (this.updating)
{
while (this.updating.get())
{
try
{
this.updating.wait();
}
catch (InterruptedException e)
{
logger.error("Error while waiting for update lock", e);
}
}
}
synchronized (this.albums)
{
return this.albums.get(name);
}
}
/**
* Obtains all albums that were written by a certain artist.
*
* @param artist - The artist to search for.
* @return - The collection on matching albums.
*/
@Override
public Collection<Album> getAlbumsFromArtist(String artist)
{
synchronized (this.updating)
{
while (this.updating.get())
{
try
{
this.updating.wait();
}
catch (InterruptedException e)
{
logger.error("Error while waiting for update lock", e);
}
}
}
synchronized (this.albums)
{
return this.albums.values().stream().filter(album -> Arrays.asList(album.artists).contains(artist)).collect(Collectors.toUnmodifiableSet());
}
}
/**
* Obtains all albums that match a certain genre
*
* @param genre - The genre to search for.
* @return - The collection on matching albums.
*/
@Override
public Collection<Album> getAlbumsFromGenre(String genre)
{
synchronized (this.updating)
{
while (this.updating.get())
{
try
{
this.updating.wait();
}
catch (InterruptedException e)
{
logger.error("Error while waiting for update lock", e);
}
}
}
synchronized (this.albums)
{
return this.albums.values().stream().filter(album -> Arrays.asList(album.genres).contains(genre)).collect(Collectors.toUnmodifiableSet());
}
}
/**
* Obtains all albums that were released a certain year.
*
* @param year - The year to search for.
* @return - The collection on matching albums.
*/
@Override
public Collection<Album> getAlbumsFromYear(int year)
{
synchronized (this.updating)
{
while (this.updating.get())
{
try
{
this.updating.wait();
}
catch (InterruptedException e)
{
logger.error("Error while waiting for update lock", e);
}
}
}
synchronized (this.albums)
{
return this.albums.values().stream().filter(album -> album.year == year).collect(Collectors.toUnmodifiableSet());
}
} }
@Override @Override
public int getUpdateProgress() public int getUpdateProgress()
{ {
return this.updatedSongs; int sup = super.getUpdateProgress();
if (sup == 0)
{
sup = this.progress.get();
}
return sup;
} }
@Override @Override
public int getTotalUpdateSongs() public int getTotalUpdates()
{ {
return this.totalUpdate; int sup = super.getTotalUpdates();
if (sup == 0)
{
sup = this.updating.get();
}
return sup;
} }
@Override @Override
public String getUpdateText() public String getUpdateText()
{ {
return ""; String sup = super.getUpdateText();
if (sup == null || sup.isEmpty())
{
sup = this.updateItem;
}
return sup;
}
/**
* Obtains the name of
*
* @return The name of the database table. This is case insensitive.
*/
@Override
protected String getDatabaseTable()
{
return "internet_songs";
}
/**
* Obtains the SQL string used to create the database table should it be
* necessary.
*
* @return The SQL command that creates the database table. It should take
* the format of "CREATE TABLE name (param1 type, param2 type);"
*/
@Override
protected String createDatabaseTable()
{
/*
* Create the table
*/
return "CREATE TABLE internet_songs" +
"(url TEXT PRIMARY KEY NOT NULL," +
"title TEXT," +
"artists TEXT," +
"track INTEGER," +
"disc INTEGER," +
"duration BIGINT," +
"album TEXT);";
} }
@Override @Override
public void addUpdateListener(UpdateListener listener) public AlbumProvider getAlbumProvider()
{ {
this.listeners.add(listener); return this.albums;
} }
/**
* Called when an entry is read from the database and is ready to be
* parsed.
* <p>
* Note that this method is called for every row. Do NOT call {@link
* ResultSet#next()}!
*
* @param result The result that is read from.
*/
@Override @Override
public void removeUpdateListener(UpdateListener listener) protected InternetSong readResult(ResultSet result) throws SQLException
{ {
this.listeners.remove(listener);
}
protected void triggerUpdateListeners()
{
this.listeners.forEach(listener -> listener.onUpdate(this.getUpdateProgress(), this.getTotalUpdateSongs(), this.getUpdateText()));
}
public Future<InternetSong> addSong(URL url, String title, String albumName, String artists, String genres)
{
return service.submit(() -> {
Album album = albums.get(albumName);
Statement state = DatabaseManager.getDb().createStatement();
if (album == null)
{
album = new Album();
album.name = albumName;
albums.put(albumName, album);
synchronized (DatabaseManager.getDb())
{
state.executeUpdate("INSERT INTO internet_albums (album) VALUES ('" + albumName + "');");
}
}
album.artists = Arrays.stream(artists.split(";")).map(String::trim).toArray(String[]::new);
album.genres = Arrays.stream(genres.split(";")).map(String::trim).toArray(String[]::new);
synchronized (DatabaseManager.getDb())
{
state.executeUpdate("UPDATE internet_albums SET artists='" + String.join(";", album.artists) + "' WHERE album='" + albumName + "';");
state.executeUpdate("UPDATE internet_albums SET genres='" + String.join(";", album.genres) + "' WHERE album='" + albumName + "';");
}
InternetSong song = new InternetSong(); InternetSong song = new InternetSong();
song.location = url; song.location = result.getURL("url");
song.title = title;
song.album = album;
song.artists = album.artists.clone();
// TODO - Evaluate the song
songs.put(url, song);
StringBuilder sql = new StringBuilder("INSERT INTO internet_songs ");
StringBuilder columns = new StringBuilder("(");
StringBuilder values = new StringBuilder("(");
columns.append("url,");
values.append('\'').append(url.toString().replaceAll("'", "''")).append("',");
if (title != null && !title.isEmpty())
{
columns.append("title,");
values.append('\'').append(title.replaceAll("'", "''")).append("',");
}
if (song.artists != null)
{
columns.append("artists,");
values.append('\'').append(String.join(";", song.artists)).append("',");
}
columns.append("album");
values.append('\'').append(albumName).append("'");
columns.append(") VALUES ");
values.append(");");
sql.append(columns);
sql.append(values);
synchronized (DatabaseManager.getDb())
{
state.executeUpdate(sql.toString());
}
this.triggerUpdateListeners();
return song;
});
}
private class SongQuery implements Runnable
{
SongQuery()
{
}
@Override
public void run()
{
Statement state;
ResultSet result;
Album album;
InternetSong song;
int numAlbums = 0, numSongs = 0;
synchronized (updating)
{
updating.set(true);
try
{
logger.debug("Querying database.");
/*
* Check if the table exists
*/
synchronized (DatabaseManager.getDb())
{
/*
* Make sure that a "null" album is available
*/
if (albums.get(null) == null)
{
album = new Album();
album.name = "Unknown";
albums.put(null, album);
}
if (albums.get("Unknown") == null)
{
albums.put("Unknown", albums.get(null));
}
state = DatabaseManager.getDb().createStatement();
result = state
.executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='INTERNET_ALBUMS';");
if (!result.next())
{
logger.debug("Creating album table.");
/*
* Create the table
*/
state.executeUpdate("CREATE TABLE INTERNET_ALBUMS" +
"(ALBUM TEXT PRIMARY KEY NOT NULL," +
"ARTISTS TEXT," +
"YEAR INTEGER," +
"GENRES TEXT," +
"TRACKS INTEGER," +
"DISCS INTEGER);");
}
else
{
result = state
.executeQuery("SELECT * FROM INTERNET_ALBUMS;");
while (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");
numAlbums++;
}
}
result = state
.executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='INTERNET_SONGS';");
if (!result.next())
{
logger.debug("Creating song table.");
/*
* Create the table
*/
state.executeUpdate("CREATE TABLE INTERNET_SONGS" +
"(URL TEXT PRIMARY KEY NOT NULL," +
"TITLE TEXT," +
"ARTISTS TEXT," +
"TRACK INTEGER," +
"DISC INTEGER," +
"DURATION BIGINT," +
"ALBUM TEXT);");
}
else
{
result = state
.executeQuery("SELECT * FROM INTERNET_SONGS;");
while (result.next())
{
if (result.getString("url") == null)
{
continue;
}
URL url;
try
{
url = new URL(result.getString("url"));
}
catch (MalformedURLException e)
{
logger.error("Could not parse URL " + result
.getString("url"), e);
continue;
}
song = songs.get(url);
if (song == null)
{
song = new InternetSong();
song.location = url;
songs.put(song.location, song);
}
song.title = result.getString("title"); song.title = result.getString("title");
song.artists = Optional song.artists =
.ofNullable(result.getString("artists")) Arrays.stream(result.getString("artists").split(";"))
.map(s -> s.split(";")) .map(String::trim).toArray(String[]::new);
.orElse(new String[0]);
song.trackNum = result.getInt("track"); song.trackNum = result.getInt("track");
song.disc = result.getInt("disc"); song.disc = result.getInt("disc");
song.duration = result.getLong("duration"); song.duration = result.getLong("duration");
song.album = Optional try
.ofNullable(result.getString("album"))
.map(albums::get)
.orElse(albums.get("Unknown"));
numSongs++;
}
}
state.close();
}
}
catch (SQLException e)
{ {
logger.error("Could not query SQL database.", e); getAlbumProvider().joinUpdate();
song.album = getAlbumProvider().getAlbumByName(result.getString(
"album"));
} }
logger.debug("Query complete, retrieved {} albums and {} songs", numAlbums, numSongs); catch (InterruptedException e)
updatedSongs = 0; {
totalUpdate = 0; logger.error("Couldn't wait for album provider for song {}", song
updating.set(false); , e);
updating.notifyAll();
} }
triggerUpdateListeners(); return song;
}
/**
* Called to obtain the properties of an object to write.
*
* @param item - The item to serialize.
* @return Properties to write.
*/
@Override
protected Map<String, Object> serializeItem(InternetSong item)
{
LinkedHashMap<String, Object> map = new LinkedHashMap<>();
map.put("url", item.location);
map.put("title", item.title);
map.put("artists", Arrays.stream(item.artists).reduce("",
(s1, s2) -> s1 + ";" + s2));
map.put("track", item.trackNum);
map.put("disc", item.disc);
map.put("duration", item.duration);
map.put("album",
Optional.ofNullable(item.album).map(a -> a.name).orElse(null));
return map;
}
/**
* Converts a piece of data into a string that will display in the update
* text.
*
* @param data - The data to stringify.
* @return A string representation of the data being updated.
*/
@Override
protected String stringifyResult(InternetSong data)
{
return Optional.ofNullable(data.album).map(a -> a.name).orElse(null) +
"/" + data.title;
}
/**
* A callback for when the database scan is complete.
* <p>
* Note that this method is called from the same thread that the scanner is
* from.
* </p>
*
* @return A fork-join task to invoke. This may be null.
*/
@Override
protected ForkJoinTask[] onComplete()
{
return null;
}
public ForkJoinTask<InternetSong> addSong(URL url)
{
AddInternetTask task = new AddInternetTask(url);
this.service.execute(task);
return task;
}
private class AddInternetTask extends ForkJoinTask<InternetSong>
{
private final URL loc;
private InternetSong item;
public AddInternetTask(URL song)
{
this.loc = song;
}
/**
* Returns the result that would be returned by {@link #join}, even if
* this task completed abnormally, or {@code null} if this task is not
* known to have been completed. This method is designed to aid
* debugging, as well as to support extensions. Its use in any other
* context is discouraged.
*
* @return the result, or {@code null} if not completed
*/
@Override
public InternetSong getRawResult()
{
return this.item;
}
/**
* Forces the given value to be returned as a result. This method is
* designed to support extensions, and should not in general be called
* otherwise.
*
* @param value the value
*/
@Override
protected void setRawResult(InternetSong value)
{
this.item = value;
}
/**
* Immediately performs the base action of this task and returns true
* if, upon return from this method, this task is guaranteed to have
* completed. This method may return false otherwise, to indicate that
* this task is not necessarily complete (or is not known to be
* complete), for example in asynchronous actions that require explicit
* invocations of completion methods. This method may also throw an
* (unchecked) exception to indicate abnormal exit. This method is
* designed to support extensions, and should not in general be called
* otherwise.
*
* @return {@code true} if this task is known to have completed normally
*/
@Override
protected boolean exec()
{
InternetSong data;
try
{
data =
(InternetSong) Browser.getInstance().sendObject(new QuerySongData(this.loc)).get();
}
catch (InterruptedException | ExecutionException | IOException e)
{
this.completeExceptionally(e);
return false;
}
if (data != null)
{
try
{
writeItem(data).get();
}
catch (InterruptedException | ExecutionException e)
{
this.completeExceptionally(e);
return false;
}
}
this.complete(data);
return true;
} }
} }
} }

View File

@@ -1,208 +0,0 @@
/*
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
*/
package edu.regis.universeplayer.data;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Random;
import java.util.stream.Collectors;
/**
* A test song database that automatically generates a handful of songs.
*
* @author William Hubbard
* @version 0.1
*/
public class SimpleSongProvider implements SongProvider<Song>
{
private ArrayList<Album> albums;
private ArrayList<Song> songs;
@Override
public Collection<Album> getAlbums()
{
if (albums == null)
{
albums = new ArrayList<>();
Random random = new Random();
StringBuilder builder;
String albumName, albumArtist;
Album album;
for (int albumNum = 0, numAlbums = random
.nextInt(20) + 20; albumNum < numAlbums; albumNum++)
{
builder = new StringBuilder();
for (int i = 0, l = random.nextInt(10) + 10; i < l; i++)
{
builder.append((char) (random.nextInt(26) + 97));
}
albumName = builder.toString();
builder = new StringBuilder();
for (int i = 0, l = random.nextInt(10) + 10; i < l; i++)
{
builder.append((char) (random.nextInt(26) + 97));
}
albumArtist = builder.toString();
album = new Album()
{
};
album.name = albumName;
album.artists = new String[]{albumArtist};
album.genres = new String[]{"Soundtrack"};
album.year = 2019;
album.totalTracks = random.nextInt(10) + 10;
album.id = albumNum;
albums.add(album);
}
}
return albums;
}
@Override
public Collection<Song> getSongs()
{
if (songs == null)
{
/*
* Generate a list of songs
*/
Random random = new Random();
StringBuilder builder;
String songTitle;
Song song;
int songNum, numSongs;
songs = new ArrayList<>();
for (Album album : this.getAlbums())
{
for (songNum = 0, numSongs = album.totalTracks; songNum < numSongs; songNum++)
{
builder = new StringBuilder();
for (int i = 0, l = random.nextInt(10) + 10; i < l; i++)
{
builder.append((char) (random.nextInt(26) + 97));
}
songTitle = builder.toString();
song = new Song()
{
};
song.title = songTitle;
song.disc = 1;
song.trackNum = songNum + 1;
song.artists = album.artists.clone();
song.album = album;
songs.add(song);
}
album.totalTracks = numSongs;
}
}
return this.songs;
}
@Override
public Collection<String> getArtists()
{
return this.songs.stream().flatMap(song -> Arrays.stream(song.artists)).sorted()
.collect(Collectors.toList());
}
@Override
public Collection<String> getGenres()
{
return this.songs.stream().flatMap(song -> Arrays.stream(song.album.genres)).sorted()
.collect(Collectors.toList());
}
@Override
public Collection<String> getAlbumArtists()
{
return this.albums.stream().flatMap(album -> Arrays.stream(album.artists)).sorted()
.collect(Collectors.toList());
}
@Override
public Collection<Integer> getYears()
{
return this.albums.stream().map(album -> album.year).sorted()
.collect(Collectors.toList());
}
@Override
public Collection<Song> getSongsFromAlbum(Album album)
{
return this.songs.stream().filter(song -> song.album == album).sorted()
.collect(Collectors.toList());
}
@Override
public Collection<Song> getSongsFromArtist(String artist)
{
return this.songs.stream().filter(song -> Arrays.asList(song.artists).contains(artist)).sorted()
.collect(Collectors.toList());
}
@Override
public Album getAlbumByName(String name)
{
return this.albums.stream().filter(album -> album.name.equals(name)).findFirst().orElse(null);
}
@Override
public Collection<Album> getAlbumsFromArtist(String artist)
{
return this.albums.stream().filter(album -> Arrays.asList(album.artists).contains(artist))
.sorted().collect(Collectors.toList());
}
@Override
public Collection<Album> getAlbumsFromGenre(String genre)
{
return this.albums.stream().filter(album -> Arrays.asList(album.genres).contains(genre))
.sorted().collect(Collectors.toList());
}
@Override
public Collection<Album> getAlbumsFromYear(int year)
{
return this.albums.stream().filter(album -> album.year == year)
.sorted().collect(Collectors.toList());
}
@Override
public int getUpdateProgress()
{
return 0;
}
@Override
public int getTotalUpdateSongs()
{
return -1;
}
@Override
public String getUpdateText()
{
return null;
}
@Override
public void addUpdateListener(UpdateListener listener)
{
// We don't need update listeners here
}
@Override
public void removeUpdateListener(UpdateListener listener)
{
// We don't need update listeners here
}
}

View File

@@ -6,6 +6,9 @@ package edu.regis.universeplayer.data;
import java.io.File; import java.io.File;
import java.util.Collection; import java.util.Collection;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
/** /**
* The song provider interface serves to give the application access to any form of song database as needed. * The song provider interface serves to give the application access to any form of song database as needed.
@@ -13,151 +16,84 @@ import java.util.Collection;
* @author William Hubbard * @author William Hubbard
* @version 0.1 * @version 0.1
*/ */
public interface SongProvider<T extends Song> public interface SongProvider<T extends Song> extends DataProvider<T>
{ {
// SongProvider<Song> INSTANCE = new CompiledSongProvider(new LocalSongProvider(new File(System.getProperty("user.home"), "Music")), InternetSongProvider.getInstance());
/** /**
* A SongProvider instance designed to * Obtains a reference to the album provider.
* @return The album provider used.
*/ */
SongProvider<Song> INSTANCE = new CompiledSongProvider(new LocalSongProvider(new File(System.getProperty("user.home"), "Music")), InternetSongProvider.getInstance()); AlbumProvider getAlbumProvider();
/** /**
* Obtains all albums within the collection. * Obtains all albums within the collection.
* *
* @return A list of albums. * @return A list of albums.
*/ */
Collection<Album> getAlbums(); default Collection<Album> getAlbums()
{
return this.getCollection().stream().map(s -> s.album).collect(Collectors
.toSet());
}
/** /**
* Obtains all songs within the collection. * Obtains all songs within the collection.
* *
* @return A list of songs. * @return A list of songs.
*/ */
Collection<T> getSongs(); default Collection<T> getSongs()
{
return this.getCollection();
}
/** /**
* Obtains a list of all artists. * Obtains a list of all artists.
* *
* @return All artists. * @return All artists.
*/ */
Collection<String> getArtists(); default Collection<String> getArtists()
{
/** return this.getCollection().stream().filter(s -> s.artists != null).map(s -> s.artists)
* Obtains a list of all album artists. .mapMulti((BiConsumer<String[], Consumer<String>>) (strings, objectConsumer) -> {
* for (String string : strings)
* @return All album artists. {
*/ objectConsumer.accept(string);
Collection<String> getAlbumArtists(); }
}).collect(Collectors.toSet());
/** }
* Obtains a list of all genres.
*
* @return All genres.
*/
Collection<String> getGenres();
/**
* Obtains a list of all years that have albums.
*
* @return All years.
*/
Collection<Integer> getYears();
/** /**
* Obtains all songs from an album. * Obtains all songs from an album.
* *
* @param album - The album to obtain * @param album - The album to obtain
* @return All songs from the requested album, or null if that album is not in the database. * @return All songs from the requested album, or null if that album is not
* in the database.
*/ */
Collection<T> getSongsFromAlbum(Album album); default Collection<T> getSongsFromAlbum(Album album)
{
return this.getCollection().stream().filter(s -> s.album == album)
.collect(Collectors.toSet());
}
/** /**
* Obtains all songs written by a given artist. * Obtains all songs written by a given artist.
* *
* @param artist - The artist to search for * @param artist - The artist to search for
* @return A list of all songs from the specified artist, or null if that artist is not in the * @return A list of all songs from the specified artist, or null if that
* database. * artist is not in the database.
*/ */
Collection<T> getSongsFromArtist(String artist); default Collection<T> getSongsFromArtist(String artist)
/**
* Obtains an album by a specific name.
*
* @param name - The name to search for.
* @return - The first album that matches the given name, or null if that album name is not in
* the database.
*/
Album getAlbumByName(String name);
/**
* Obtains all albums that were written by a certain artist.
*
* @param artist - The artist to search for.
* @return - The collection on matching albums.
*/
Collection<Album> getAlbumsFromArtist(String artist);
/**
* Obtains all albums that match a certain genre
*
* @param genre - The genre to search for.
* @return - The collection on matching albums.
*/
Collection<Album> getAlbumsFromGenre(String genre);
/**
* Obtains all albums that were released a certain year.
*
* @param year - The year to search for.
* @return - The collection on matching albums.
*/
Collection<Album> getAlbumsFromYear(int year);
/**
* Checks to see if we are updating any songs.
*
* @return True if we are updating the song cache.
*/
default boolean isUpdating()
{ {
return this.getTotalUpdateSongs() != 0; return this.getCollection().stream().filter(s -> {
for (int i = 0; i < s.artists.length; i++)
{
if (s.artists[i].equals(artist))
{
return true;
}
}
return false;
}).collect(Collectors.toSet());
} }
/**
* When we are updating the song cache, this method obtains the number of
* songs already updated.
*
* @return The number of songs successfully updated.
*/
int getUpdateProgress();
/**
* Determines whether or not we are updating the cache and, if so, gets the
* total number of songs we need to update
*
* @return The total number of songs to update. A 0 means that we do not
* have any songs to update, and a negative number means that we are
* currently calculating how many songs we need to update.
*/
int getTotalUpdateSongs();
/**
* Determines the text displayed for the progress of updates
*
* @return The update status text.
*/
String getUpdateText();
/**
* Adds a listener for song updates.
*
* @param listener - The listener to add.
*/
void addUpdateListener(UpdateListener listener);
/**
* Removes a listener for song updates.
*
* @param listener - The listener to remove.
*/
void removeUpdateListener(UpdateListener listener);
} }

View File

@@ -15,10 +15,12 @@ public interface UpdateListener
{ {
/** /**
* Called when the update status of the player has changed. * Called when the update status of the player has changed.
* @param provider - The provider that triggered the listener.
* @param updated - The number of songs updated. * @param updated - The number of songs updated.
* @param totalUpdate - The total number of songs to update, or -1 if we are * @param totalUpdate - The total number of songs to update, or -1 if we are
* still determining that. * still determining that.
* @param updating - The text to display on update bars. * @param updating - The text to display on update bars.
*/ */
void onUpdate(int updated, int totalUpdate, String updating); <T> void onUpdate(DataProvider<T> provider, int updated, int totalUpdate,
String updating);
} }

View File

@@ -15,7 +15,9 @@ import javax.swing.*;
import com.wordpress.tips4java.ScrollablePanel; import com.wordpress.tips4java.ScrollablePanel;
import edu.regis.universeplayer.PlayerEnvironment;
import edu.regis.universeplayer.data.Album; import edu.regis.universeplayer.data.Album;
import edu.regis.universeplayer.data.AlbumProvider;
import edu.regis.universeplayer.data.CollectionType; import edu.regis.universeplayer.data.CollectionType;
import edu.regis.universeplayer.data.Song; import edu.regis.universeplayer.data.Song;
import edu.regis.universeplayer.data.SongProvider; import edu.regis.universeplayer.data.SongProvider;
@@ -144,15 +146,15 @@ public class CollectionList extends ScrollablePanel
artistLabel.addActionListener(mouseEvent -> { artistLabel.addActionListener(mouseEvent -> {
if (album) if (album)
{ {
this.triggerSongDisplayListeners(SongProvider.INSTANCE this.triggerSongDisplayListeners(PlayerEnvironment.getAlbums()
.getAlbumsFromArtist(artist).stream() .getAlbumsFromArtist(artist).stream()
.flatMap(album2 -> SongProvider.INSTANCE.getSongsFromAlbum(album2) .flatMap(album2 -> PlayerEnvironment.getSongs().getSongsFromAlbum(album2)
.stream()) .stream())
.collect(Collectors.toList())); .collect(Collectors.toList()));
} }
else else
{ {
this.triggerSongDisplayListeners(new ArrayList<>(SongProvider.INSTANCE this.triggerSongDisplayListeners(new ArrayList<>(PlayerEnvironment.getSongs()
.getSongsFromArtist(artist))); .getSongsFromArtist(artist)));
} }
}); });
@@ -184,7 +186,7 @@ public class CollectionList extends ScrollablePanel
albumLabel.setText(album.name); albumLabel.setText(album.name);
albumLabel.setHorizontalTextPosition(JLabel.CENTER); albumLabel.setHorizontalTextPosition(JLabel.CENTER);
albumLabel.setVerticalTextPosition(JLabel.BOTTOM); albumLabel.setVerticalTextPosition(JLabel.BOTTOM);
albumLabel.addActionListener(mouseEvent -> this.triggerSongDisplayListeners(new ArrayList<>(SongProvider.INSTANCE albumLabel.addActionListener(mouseEvent -> this.triggerSongDisplayListeners(new ArrayList<>(PlayerEnvironment.getSongs()
.getSongsFromAlbum(album)))); .getSongsFromAlbum(album))));
this.add(albumLabel); this.add(albumLabel);
this.labelMap.put(albumLabel, album); this.labelMap.put(albumLabel, album);
@@ -222,9 +224,9 @@ public class CollectionList extends ScrollablePanel
genreLabel.setText(genre); genreLabel.setText(genre);
genreLabel.setHorizontalTextPosition(JLabel.CENTER); genreLabel.setHorizontalTextPosition(JLabel.CENTER);
genreLabel.setVerticalTextPosition(JLabel.BOTTOM); genreLabel.setVerticalTextPosition(JLabel.BOTTOM);
genreLabel.addActionListener(mouseEvent -> this.triggerSongDisplayListeners(SongProvider.INSTANCE genreLabel.addActionListener(mouseEvent -> this.triggerSongDisplayListeners(PlayerEnvironment.getAlbums()
.getAlbumsFromGenre(genre).stream() .getAlbumsFromGenre(genre).stream()
.flatMap(album2 -> SongProvider.INSTANCE.getSongsFromAlbum(album2) .flatMap(album2 -> PlayerEnvironment.getSongs().getSongsFromAlbum(album2)
.stream()) .stream())
.collect(Collectors.toList()))); .collect(Collectors.toList())));
this.add(genreLabel); this.add(genreLabel);
@@ -263,9 +265,9 @@ public class CollectionList extends ScrollablePanel
yearLabel.setText(year.toString()); yearLabel.setText(year.toString());
yearLabel.setHorizontalTextPosition(JLabel.CENTER); yearLabel.setHorizontalTextPosition(JLabel.CENTER);
yearLabel.setVerticalTextPosition(JLabel.BOTTOM); yearLabel.setVerticalTextPosition(JLabel.BOTTOM);
yearLabel.addActionListener(mouseEvent -> this.triggerSongDisplayListeners(SongProvider.INSTANCE yearLabel.addActionListener(mouseEvent -> this.triggerSongDisplayListeners(PlayerEnvironment.getAlbums()
.getAlbumsFromYear(year).stream() .getAlbumsFromYear(year).stream()
.flatMap(album2 -> SongProvider.INSTANCE.getSongsFromAlbum(album2) .flatMap(album2 -> PlayerEnvironment.getSongs().getSongsFromAlbum(album2)
.stream()) .stream())
.collect(Collectors.toList()))); .collect(Collectors.toList())));
this.add(yearLabel); this.add(yearLabel);

View File

@@ -4,6 +4,7 @@
package edu.regis.universeplayer.gui; package edu.regis.universeplayer.gui;
import edu.regis.universeplayer.PlayerEnvironment;
import edu.regis.universeplayer.player.Player; import edu.regis.universeplayer.player.Player;
import edu.regis.universeplayer.browser.Browser; import edu.regis.universeplayer.browser.Browser;
import edu.regis.universeplayer.player.BrowserPlayer; import edu.regis.universeplayer.player.BrowserPlayer;
@@ -137,7 +138,7 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
/** /**
* Invoked when a window has been closed. * Invoked when a window has been closed.
* *
* @param e * @param e - Event data
*/ */
@Override @Override
public void windowClosed(WindowEvent e) public void windowClosed(WindowEvent e)
@@ -234,10 +235,12 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
@Override @Override
protected Void doInBackground() throws Exception protected Void doInBackground() throws Exception
{ {
QueryFuture<Void> command = PlayerManager.getPlayers().throwError(false); QueryFuture<Void> command = PlayerManager
.getPlayers().throwError(false);
try try
{ {
if (!command.getConfirmation().wasSuccessful()) if (!command.getConfirmation()
.wasSuccessful())
{ {
logger.error("Could not run command", logger.error("Could not run command",
command.getConfirmation() command.getConfirmation()
@@ -295,7 +298,7 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
if (this.isEnabled()) if (this.isEnabled())
{ {
collectionTypes collectionTypes
.triggerSongDisplayListeners(new ArrayList<>(SongProvider.INSTANCE .triggerSongDisplayListeners(new ArrayList<>(PlayerEnvironment.getSongs()
.getSongs())); .getSongs()));
} }
} }
@@ -311,7 +314,7 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
if (this.isEnabled()) if (this.isEnabled())
{ {
collectionTypes collectionTypes
.triggerCollectionDisplayListeners(CollectionType.albumArtist, SongProvider.INSTANCE .triggerCollectionDisplayListeners(CollectionType.albumArtist, PlayerEnvironment.getAlbums()
.getAlbumArtists()); .getAlbumArtists());
} }
} }
@@ -327,7 +330,7 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
if (this.isEnabled()) if (this.isEnabled())
{ {
collectionTypes collectionTypes
.triggerCollectionDisplayListeners(CollectionType.album, SongProvider.INSTANCE .triggerCollectionDisplayListeners(CollectionType.album, PlayerEnvironment.getSongs()
.getAlbums()); .getAlbums());
} }
} }
@@ -343,7 +346,7 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
if (this.isEnabled()) if (this.isEnabled())
{ {
collectionTypes collectionTypes
.triggerCollectionDisplayListeners(CollectionType.genre, SongProvider.INSTANCE .triggerCollectionDisplayListeners(CollectionType.genre, PlayerEnvironment.getAlbums()
.getGenres()); .getGenres());
} }
} }
@@ -359,7 +362,7 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
if (this.isEnabled()) if (this.isEnabled())
{ {
collectionTypes collectionTypes
.triggerCollectionDisplayListeners(CollectionType.year, SongProvider.INSTANCE .triggerCollectionDisplayListeners(CollectionType.year, PlayerEnvironment.getAlbums()
.getYears()); .getYears());
} }
} }
@@ -652,12 +655,14 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
} }
@Override @Override
public void onUpdate(int updated, int totalUpdate, String updating) public <T> void onUpdate(DataProvider<T> provider, int updated,
int totalUpdate, String updating)
{ {
this.controls.setUpdateProgress(updated, totalUpdate, updating); this.controls.setUpdateProgress(updated, totalUpdate, updating);
if (updated == totalUpdate || totalUpdate == 0) if (updated == totalUpdate || totalUpdate == 0)
{ {
Collection<Song> songs = SongProvider.INSTANCE.getSongs(); Collection<? extends Song> songs =
PlayerEnvironment.getSongs().getSongs();
logger.debug("Resetting the song provider with {} songs.", logger.debug("Resetting the song provider with {} songs.",
songs.size()); songs.size());
/* /*

View File

@@ -145,7 +145,7 @@ public class InternetSongDialog extends JDialog
} }
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
Future<InternetSong> future = InternetSongProvider.getInstance().addSong(url, this.titleBox.getText(), this.albumBox.getText(), this.artistBox.getText(), this.genreBox.getText()); Future<InternetSong> future = InternetSongProvider.getInstance().addSong(url);
InternetSong song = null; InternetSong song = null;
try try
{ {

View File

@@ -271,9 +271,9 @@ public class PlayerControls extends JPanel implements PlaybackListener
CommandConfirmation confirmation = status.getConfirmation(); CommandConfirmation confirmation = status.getConfirmation();
if (status.getConfirmation().wasSuccessful()) if (status.getConfirmation().wasSuccessful())
{ {
switch (status.get()) if (status.get() == PlaybackStatus.PLAYING)
{ {
case PLAYING -> confirmation = confirmation =
PlayerManager.getPlayers().pause() PlayerManager.getPlayers().pause()
.getConfirmation(); .getConfirmation();
} }
@@ -379,7 +379,7 @@ public class PlayerControls extends JPanel implements PlaybackListener
void setUpdateProgress(int updated, int toUpdate, String updating) void setUpdateProgress(int updated, int toUpdate, String updating)
{ {
this.updateProgress.setString(updating); this.updateProgress.setString(updating);
if (toUpdate == 0) if (toUpdate == 0 || updated == toUpdate)
{ {
this.updateProgress.setVisible(false); this.updateProgress.setVisible(false);
// this.updateProgress.setPreferredSize(new Dimension(this.updateProgress.getPreferredSize().width, 0)); // this.updateProgress.setPreferredSize(new Dimension(this.updateProgress.getPreferredSize().width, 0));
@@ -399,6 +399,7 @@ public class PlayerControls extends JPanel implements PlaybackListener
this.updateProgress.setValue(updated); this.updateProgress.setValue(updated);
} }
} }
this.repaint();
} }
/** /**

View File

@@ -10,6 +10,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import edu.regis.universeplayer.ClickListener; import edu.regis.universeplayer.ClickListener;
import edu.regis.universeplayer.PlayerEnvironment;
import edu.regis.universeplayer.data.Queue; import edu.regis.universeplayer.data.Queue;
import edu.regis.universeplayer.data.*; import edu.regis.universeplayer.data.*;
@@ -44,7 +45,7 @@ public class SongList extends ScrollablePanel
this.setFocusTraversalPolicyProvider(true); this.setFocusTraversalPolicyProvider(true);
this.setLayout(layout); this.setLayout(layout);
SongProvider<?> provider = SongProvider.INSTANCE; SongProvider<?> provider = PlayerEnvironment.getSongs();
this.listAlbums(provider.getSongs()); this.listAlbums(provider.getSongs());
this.setScrollableWidth(ScrollableSizeHint.FIT); this.setScrollableWidth(ScrollableSizeHint.FIT);
@@ -98,15 +99,19 @@ public class SongList extends ScrollablePanel
* thread. * thread.
* *
* @return the computed result * @return the computed result
* @throws Exception if unable to compute a result
*/ */
@Override @Override
protected Object doInBackground() throws Exception protected Object doInBackground()
{
try
{ {
logger.debug("Sorting {} songs...", songs.size()); logger.debug("Sorting {} songs...", songs.size());
Map<Album, List<Song>> albums = songs.stream().sorted().collect(Collectors Map<Album, List<Song>> albums =
songs.stream().filter(s -> s.album != null).sorted()
.collect(Collectors
.groupingBy(song -> song.album, Collectors .groupingBy(song -> song.album, Collectors
.mapping(song -> (Song) song, Collectors.toList()))); .mapping(song -> (Song) song, Collectors
.toList())));
logger.debug("Listing {} albums ({} songs)", logger.debug("Listing {} albums ({} songs)",
albums.size(), songs.size()); albums.size(), songs.size());
GridBagConstraints c = new GridBagConstraints(); GridBagConstraints c = new GridBagConstraints();
@@ -133,14 +138,14 @@ public class SongList extends ScrollablePanel
c.weightx = 0; c.weightx = 0;
c.anchor = GridBagConstraints.NORTHWEST; c.anchor = GridBagConstraints.NORTHWEST;
c.insets = new Insets(0, 0, 20, 10); c.insets = new Insets(0, 0, 20, 10);
List<Song> finalSongCollection = songCollection;
albumInfo.addMouseListener((ClickListener) e -> { albumInfo.addMouseListener((ClickListener) e -> {
if (e.getClickCount() == 2) if (e.getClickCount() == 2)
{ {
Queue.getInstance().addAll(finalSongCollection); Queue.getInstance().addAll(songCollection);
} }
}); });
albumInfo.albumName.addMouseListener((ClickListener) e -> { albumInfo.albumName
.addMouseListener((ClickListener) e -> {
if (e.getClickCount() == 2) if (e.getClickCount() == 2)
{ {
Container inter = SongList.this; Container inter = SongList.this;
@@ -152,12 +157,14 @@ public class SongList extends ScrollablePanel
.getParent() != null); .getParent() != null);
if (inter instanceof Interface) if (inter instanceof Interface)
{ {
((Interface) inter).updateSongs(SongProvider.INSTANCE ((Interface) inter)
.updateSongs(PlayerEnvironment.getSongs()
.getSongsFromAlbum(albumInfo.album)); .getSongsFromAlbum(albumInfo.album));
} }
} }
}); });
albumInfo.artists.addMouseListener((ClickListener) e -> { albumInfo.artists
.addMouseListener((ClickListener) e -> {
if (e.getClickCount() == 2) if (e.getClickCount() == 2)
{ {
Container inter = SongList.this; Container inter = SongList.this;
@@ -172,10 +179,11 @@ public class SongList extends ScrollablePanel
((Interface) inter) ((Interface) inter)
.updateCollections(CollectionType.album, Arrays .updateCollections(CollectionType.album, Arrays
.stream(albumInfo.album.artists) .stream(albumInfo.album.artists)
.flatMap(s -> SongProvider.INSTANCE .flatMap(s -> PlayerEnvironment.getAlbums()
.getAlbumsFromArtist(s) .getAlbumsFromArtist(s)
.stream()) .stream())
.collect(Collectors.toList())); .collect(Collectors
.toList()));
} }
} }
}); });
@@ -194,9 +202,11 @@ public class SongList extends ScrollablePanel
((Interface) inter) ((Interface) inter)
.updateCollections(CollectionType.album, Arrays .updateCollections(CollectionType.album, Arrays
.stream(albumInfo.album.genres) .stream(albumInfo.album.genres)
.flatMap(s -> SongProvider.INSTANCE .flatMap(s -> PlayerEnvironment.getAlbums()
.getAlbumsFromGenre(s).stream()) .getAlbumsFromGenre(s)
.collect(Collectors.toList())); .stream())
.collect(Collectors
.toList()));
} }
} }
}); });
@@ -213,12 +223,13 @@ public class SongList extends ScrollablePanel
if (inter instanceof Interface) if (inter instanceof Interface)
{ {
((Interface) inter) ((Interface) inter)
.updateCollections(CollectionType.album, SongProvider.INSTANCE .updateCollections(CollectionType.album, PlayerEnvironment.getAlbums()
.getAlbumsFromYear(albumInfo.album.year)); .getAlbumsFromYear(albumInfo.album.year));
} }
} }
}); });
albumInfos.put(albumInfo, (GridBagConstraints) c.clone()); albumInfos
.put(albumInfo, (GridBagConstraints) c.clone());
artMap.put(albumInfo, album); artMap.put(albumInfo, album);
JButton firstSong = null; JButton firstSong = null;
@@ -236,7 +247,8 @@ public class SongList extends ScrollablePanel
c.weightx = 0; c.weightx = 0;
c.anchor = GridBagConstraints.NORTHEAST; c.anchor = GridBagConstraints.NORTHEAST;
c.insets = new Insets(0, 0, 0, 0); c.insets = new Insets(0, 0, 0, 0);
albumInfos.put(songNum, (GridBagConstraints) c.clone()); albumInfos.put(songNum, (GridBagConstraints) c
.clone());
labelMap.put(songNum, song); labelMap.put(songNum, song);
songTitle = new JButton(song.title); songTitle = new JButton(song.title);
@@ -244,7 +256,8 @@ public class SongList extends ScrollablePanel
{ {
if (song instanceof LocalSong) if (song instanceof LocalSong)
{ {
songTitle.setText(((LocalSong) song).file.getName()); songTitle.setText(((LocalSong) song).file
.getName());
} }
} }
songTitle.setHorizontalAlignment(JButton.LEFT); songTitle.setHorizontalAlignment(JButton.LEFT);
@@ -260,7 +273,8 @@ public class SongList extends ScrollablePanel
{ {
Queue.getInstance().add(song); Queue.getInstance().add(song);
Queue.getInstance() Queue.getInstance()
.skipToSong(Queue.getInstance().size() - 1); .skipToSong(Queue.getInstance()
.size() - 1);
} }
}); });
c.gridx = 2; c.gridx = 2;
@@ -268,7 +282,8 @@ public class SongList extends ScrollablePanel
c.weightx = 1.0; c.weightx = 1.0;
c.anchor = GridBagConstraints.NORTHWEST; c.anchor = GridBagConstraints.NORTHWEST;
c.insets = new Insets(0, 10, 0, 0); c.insets = new Insets(0, 10, 0, 0);
albumInfos.put(songTitle, (GridBagConstraints) c.clone()); albumInfos.put(songTitle, (GridBagConstraints) c
.clone());
labelMap.put(songTitle, song); labelMap.put(songTitle, song);
// TODO - Add song length or something // TODO - Add song length or something
@@ -284,7 +299,6 @@ public class SongList extends ScrollablePanel
finalFirstSong.requestFocusInWindow(); finalFirstSong.requestFocusInWindow();
} }
}); });
List<Song> finalSongCollection1 = songCollection;
albumInfo.addKeyListener(new KeyAdapter() albumInfo.addKeyListener(new KeyAdapter()
{ {
@Override @Override
@@ -293,13 +307,15 @@ public class SongList extends ScrollablePanel
if (e.getKeyCode() == KeyEvent.VK_ENTER) if (e.getKeyCode() == KeyEvent.VK_ENTER)
{ {
Queue.getInstance() Queue.getInstance()
.addAll(finalSongCollection1); .addAll(songCollection);
} }
} }
}); });
} }
i.getAndIncrement(); i.getAndIncrement();
this.setProgress((int) (numSongs.incrementAndGet() / (float) songs.size() * 100F)); this.setProgress((int) (numSongs
.incrementAndGet() / (float) songs
.size() * 100F));
} }
// this.add(new JLabel(new ImageIcon(this.getClass().getResource("/gui/icons/defaultart.png"), "Default")), c); // this.add(new JLabel(new ImageIcon(this.getClass().getResource("/gui/icons/defaultart.png"), "Default")), c);
@@ -307,7 +323,8 @@ public class SongList extends ScrollablePanel
c.gridy = i.getAndIncrement(); c.gridy = i.getAndIncrement();
c.gridwidth = 3; c.gridwidth = 3;
c.anchor = GridBagConstraints.NORTH; c.anchor = GridBagConstraints.NORTH;
albumInfos.put(new JSeparator(SwingConstants.HORIZONTAL), albumInfos
.put(new JSeparator(SwingConstants.HORIZONTAL),
(GridBagConstraints) c.clone()); (GridBagConstraints) c.clone());
i.getAndIncrement(); i.getAndIncrement();
@@ -315,14 +332,18 @@ public class SongList extends ScrollablePanel
logger.debug("Song list built {} components", logger.debug("Song list built {} components",
albumInfos.size()); albumInfos.size());
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
albumInfos.forEach((component, c1) -> { albumInfos.forEach((component, c1) -> add(component, c1));
add(component, c1);
});
revalidate(); revalidate();
logger.debug("Components added"); logger.debug("Components added");
}); });
return null; return null;
} }
catch (Exception e)
{
logger.error("Could not list songs", e);
return null;
}
}
}; };
worker.execute(); worker.execute();
return worker; return worker;

View File

@@ -110,9 +110,8 @@ public class BrowserPlayer implements Player<InternetSong>, UpdateListener
{ {
try try
{ {
QueryFuture<Void> future = new ForwardedFuture(getBrowser() return (QueryFuture<Void>) new ForwardedFuture(getBrowser()
.sendObject(new CommandQuit())); .sendObject(new CommandQuit()));
return future;
} }
catch (IOException e) catch (IOException e)
{ {
@@ -190,10 +189,21 @@ public class BrowserPlayer implements Player<InternetSong>, UpdateListener
{ {
try try
{ {
QueryFuture<PlaybackStatus> future = this.getStatus();
switch (future.get())
{
case PLAYING -> {
return new ForwardedFuture(getBrowser() return new ForwardedFuture(getBrowser()
.sendObject(new CommandSetPlayback(CommandSetPlayback.Playback.PAUSE))); .sendObject(new CommandSetPlayback(CommandSetPlayback.Playback.PAUSE)));
} }
catch (IOException e) case PAUSED, STOPPED, FINISHED -> {
return new ForwardedFuture(getBrowser()
.sendObject(new CommandSetPlayback(CommandSetPlayback.Playback.PLAY)));
}
}
return null;
}
catch (IOException | InterruptedException | ExecutionException e)
{ {
logger.error("Could not send message", e); logger.error("Could not send message", e);
return null; return null;
@@ -326,7 +336,7 @@ public class BrowserPlayer implements Player<InternetSong>, UpdateListener
* *
* @param forward - Whether the error should be thrown from the foreground * @param forward - Whether the error should be thrown from the foreground
* script or the background script. * script or the background script.
* @return * @return The return value containing error details.
*/ */
public QueryFuture<Void> throwError(boolean forward) public QueryFuture<Void> throwError(boolean forward)
{ {
@@ -349,6 +359,7 @@ public class BrowserPlayer implements Player<InternetSong>, UpdateListener
if (object instanceof PlaybackInfo) if (object instanceof PlaybackInfo)
{ {
status = new PlaybackEvent(this, (PlaybackInfo) object); status = new PlaybackEvent(this, (PlaybackInfo) object);
logger.info("Internet playback {}", status.getInfo());
this.listeners.forEach(l -> l.onPlaybackChanged(status)); this.listeners.forEach(l -> l.onPlaybackChanged(status));
} }
} }

View File

@@ -21,6 +21,8 @@ albumInfo.artists=Artists
albumInfo.genres=Genres albumInfo.genres=Genres
albumInfo.year=Year albumInfo.year=Year
update.database=Querying Database: %s
update.local=Querying File System: %s
interface.queue.title=Queue interface.queue.title=Queue