From 2e2748f9e9af4064671d1f3f68bdc17a8090401a Mon Sep 17 00:00:00 2001 From: Markil3 <75867393+Markil3@users.noreply.github.com> Date: Sat, 18 Sep 2021 15:00:12 -0600 Subject: [PATCH] Merges the two album tables into one. Albums are independent of how the song is stored. We don't need to separate them. --- .../edu/regis/universeplayer/addon/Main.java | 4 +- .../regis/universeplayer/ConfigManager.java | 156 +- .../regis/universeplayer/PlaybackInfo.java | 10 + .../browserCommands/QuerySongData.java | 73 + .../edu/regis/universeplayer/data/Album.java | 18 +- .../universeplayer/data/InternetSong.java | 6 +- .../regis/universeplayer/data/LocalSong.java | 14 + .../edu/regis/universeplayer/data/Song.java | 19 + .../universeplayer/PlayerEnvironment.java | 34 +- .../universeplayer/data/AlbumProvider.java | 77 + .../data/CompiledSongProvider.java | 375 +--- .../universeplayer/data/DataProvider.java | 73 + .../universeplayer/data/DatabaseProvider.java | 488 +++++ .../data/DefaultAlbumProvider.java | 282 +++ .../data/InternetSongProvider.java | 853 +++----- .../data/LocalSongProvider.java | 1769 ++++++----------- .../data/SimpleSongProvider.java | 208 -- .../universeplayer/data/SongProvider.java | 170 +- .../universeplayer/data/UpdateListener.java | 4 +- .../universeplayer/gui/CollectionList.java | 18 +- .../regis/universeplayer/gui/Interface.java | 25 +- .../gui/InternetSongDialog.java | 2 +- .../universeplayer/gui/PlayerControls.java | 11 +- .../regis/universeplayer/gui/SongList.java | 415 ++-- .../universeplayer/player/BrowserPlayer.java | 23 +- .../main/resources/lang/interface.properties | 2 + 26 files changed, 2553 insertions(+), 2576 deletions(-) create mode 100644 browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/QuerySongData.java create mode 100644 interface/src/main/java/edu/regis/universeplayer/data/AlbumProvider.java create mode 100644 interface/src/main/java/edu/regis/universeplayer/data/DataProvider.java create mode 100644 interface/src/main/java/edu/regis/universeplayer/data/DatabaseProvider.java create mode 100644 interface/src/main/java/edu/regis/universeplayer/data/DefaultAlbumProvider.java delete mode 100644 interface/src/main/java/edu/regis/universeplayer/data/SimpleSongProvider.java diff --git a/addonInter/src/main/java/edu/regis/universeplayer/addon/Main.java b/addonInter/src/main/java/edu/regis/universeplayer/addon/Main.java index a014d55..2e4c8b9 100644 --- a/addonInter/src/main/java/edu/regis/universeplayer/addon/Main.java +++ b/addonInter/src/main/java/edu/regis/universeplayer/addon/Main.java @@ -45,10 +45,10 @@ public class Main logger.debug("Interface socket closed, shutting down"); return true; } - /** + /* * Make sure that it is active. */ - this.sendUpdate("ping"); +// this.sendUpdate("ping"); return false; } diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/ConfigManager.java b/browserCommands/src/main/java/edu/regis/universeplayer/ConfigManager.java index bdb16c1..5a6423d 100644 --- a/browserCommands/src/main/java/edu/regis/universeplayer/ConfigManager.java +++ b/browserCommands/src/main/java/edu/regis/universeplayer/ConfigManager.java @@ -6,6 +6,15 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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 { @@ -23,6 +32,13 @@ public class ConfigManager private static File appDir; 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. * @@ -53,7 +69,8 @@ public class ConfigManager } else { - firefoxDir = new File(firefoxDir.getParentFile().getParent(), + firefoxDir = new File(firefoxDir.getParentFile() + .getParent(), "firefox"); if (firefoxDir.exists()) { @@ -155,4 +172,141 @@ public class ConfigManager } 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; + } } diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/PlaybackInfo.java b/browserCommands/src/main/java/edu/regis/universeplayer/PlaybackInfo.java index b42c737..92a2806 100644 --- a/browserCommands/src/main/java/edu/regis/universeplayer/PlaybackInfo.java +++ b/browserCommands/src/main/java/edu/regis/universeplayer/PlaybackInfo.java @@ -60,4 +60,14 @@ public class PlaybackInfo implements Serializable { return this.status; } + + @Override + public String toString() + { + return "PlaybackInfo{" + + "playTime=" + playTime + + ", status=" + status + + ", currentSong=" + currentSong + + '}'; + } } diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/QuerySongData.java b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/QuerySongData.java new file mode 100644 index 0000000..f5ea271 --- /dev/null +++ b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/QuerySongData.java @@ -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 +{ + /** + * 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 getReturnType() + { + return InternetSong.class; + } + + /** + * Gets the location of the song to query. + * + * @return The song location. + */ + public URL getUrl() + { + return this.url; + } +} diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/data/Album.java b/browserCommands/src/main/java/edu/regis/universeplayer/data/Album.java index 562b8bf..0940168 100644 --- a/browserCommands/src/main/java/edu/regis/universeplayer/data/Album.java +++ b/browserCommands/src/main/java/edu/regis/universeplayer/data/Album.java @@ -24,11 +24,25 @@ public class Album implements Comparable { if (o != null && o.name != null) { - return this.name.compareToIgnoreCase(o.name); + if (this.name == null) + { + return 1; + } + else + { + return this.name.compareToIgnoreCase(o.name); + } } else { - return -1; + if (this.name == null) + { + return 0; + } + else + { + return -1; + } } } diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/data/InternetSong.java b/browserCommands/src/main/java/edu/regis/universeplayer/data/InternetSong.java index f60f152..afb400f 100644 --- a/browserCommands/src/main/java/edu/regis/universeplayer/data/InternetSong.java +++ b/browserCommands/src/main/java/edu/regis/universeplayer/data/InternetSong.java @@ -15,6 +15,10 @@ public class InternetSong extends Song { private static final Logger logger = LoggerFactory .getLogger(InternetSong.class); + + /** + * The location of the song. + */ public URL location; @Override @@ -23,7 +27,7 @@ public class InternetSong extends Song int compare = super.compareTo(o); if (compare == 0) { - if (o instanceof LocalSong) + if (o instanceof InternetSong) { try { diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/data/LocalSong.java b/browserCommands/src/main/java/edu/regis/universeplayer/data/LocalSong.java index 73ecb0e..5fa96f1 100644 --- a/browserCommands/src/main/java/edu/regis/universeplayer/data/LocalSong.java +++ b/browserCommands/src/main/java/edu/regis/universeplayer/data/LocalSong.java @@ -12,9 +12,23 @@ import java.util.Arrays; */ public class LocalSong extends Song { + /** + * The file the song is stored at. + */ public File file; + /** + * The format type the song is stored in. + */ public String type; + /** + * The encoding format the song is recorded in. + */ public String codec; + /** + * The last modification time of this song file, as returned by {@link + * File#lastModified()}. + */ + public long lastMod; @Override public int compareTo(Song o) diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/data/Song.java b/browserCommands/src/main/java/edu/regis/universeplayer/data/Song.java index 42c4bee..8a79cc5 100644 --- a/browserCommands/src/main/java/edu/regis/universeplayer/data/Song.java +++ b/browserCommands/src/main/java/edu/regis/universeplayer/data/Song.java @@ -12,10 +12,29 @@ import java.util.Arrays; */ public class Song implements Comparable, Serializable { + /** + * The internal ID representing this song in the database. + */ + public int id; + /** + * The name of the song. + */ public String title; + /** + * Artists who contributed to the song. + */ public String[] artists; + /** + * Which track number in the album the song belongs to. + */ public int trackNum; + /** + * Which disc + */ public int disc; + /** + * How long the song is in milliseconds. + */ public long duration; /** diff --git a/interface/src/main/java/edu/regis/universeplayer/PlayerEnvironment.java b/interface/src/main/java/edu/regis/universeplayer/PlayerEnvironment.java index a8d2be6..eeae7d3 100644 --- a/interface/src/main/java/edu/regis/universeplayer/PlayerEnvironment.java +++ b/interface/src/main/java/edu/regis/universeplayer/PlayerEnvironment.java @@ -23,6 +23,11 @@ import java.util.stream.Collectors; import javax.swing.JOptionPane; 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.Song; import edu.regis.universeplayer.data.SongProvider; @@ -42,6 +47,19 @@ public class PlayerEnvironment private static final ResourceBundle langs = ResourceBundle .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. */ @@ -124,6 +142,11 @@ public class PlayerEnvironment InstanceConnector connector = new InstanceConnector(); new Thread(connector).start(); + ALBUMS_INSTANCE = new DefaultAlbumProvider(); + SONGS_INSTANCE = + new CompiledSongProvider(new LocalSongProvider(ALBUMS_INSTANCE), + new InternetSongProvider(ALBUMS_INSTANCE)); + Queue queue = Queue.getInstance(); PlayerManager playback = PlayerManager.getPlayers(); queue.addSongChangeListener(queue1 -> { @@ -185,9 +208,10 @@ public class PlayerEnvironment } }); 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(); inter.setSize(700, 500); - SongProvider.INSTANCE.addUpdateListener(inter); + SONGS_INSTANCE.addUpdateListener(inter); inter.setVisible(true); } @@ -222,7 +246,7 @@ public class PlayerEnvironment Map options, List params) { - int equals = -1; + int equals; Object value; String key; String valStr; @@ -424,7 +448,7 @@ public class PlayerEnvironment for (String param : params) { String finalParam = param.toLowerCase(); - SongProvider.INSTANCE.getSongs().forEach(song -> { + getSongs().getSongs().forEach(song -> { Integer matches = matchMap.get(song); if (matches == null) { diff --git a/interface/src/main/java/edu/regis/universeplayer/data/AlbumProvider.java b/interface/src/main/java/edu/regis/universeplayer/data/AlbumProvider.java new file mode 100644 index 0000000..dc27b05 --- /dev/null +++ b/interface/src/main/java/edu/regis/universeplayer/data/AlbumProvider.java @@ -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 +{ + /** + * Obtains all albums within the collection. + * + * @return A list of albums. + */ + Collection getAlbums(); + + /** + * Obtains a list of all album artists. + * + * @return All album artists. + */ + Collection getAlbumArtists(); + + /** + * Obtains a list of all genres. + * + * @return All genres. + */ + Collection getGenres(); + + /** + * Obtains a list of all years that have albums. + * + * @return All years. + */ + Collection 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 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 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 getAlbumsFromYear(int year); + + /** + * Writes an album to the collection. + * @param album - The album to add. + */ + Future writeItem(Album album); +} diff --git a/interface/src/main/java/edu/regis/universeplayer/data/CompiledSongProvider.java b/interface/src/main/java/edu/regis/universeplayer/data/CompiledSongProvider.java index 3a70fc9..a0e763f 100644 --- a/interface/src/main/java/edu/regis/universeplayer/data/CompiledSongProvider.java +++ b/interface/src/main/java/edu/regis/universeplayer/data/CompiledSongProvider.java @@ -5,6 +5,7 @@ package edu.regis.universeplayer.data; import java.util.*; +import java.util.stream.Collectors; /** * A song provider that serves as a central point for any and all song @@ -13,153 +14,51 @@ import java.util.*; * @author William Hubbard * @version 0.1 */ -public class CompiledSongProvider implements SongProvider +public class CompiledSongProvider implements SongProvider, UpdateListener { private final LinkedList listeners = new LinkedList<>(); - + /** * A set of all providers we pull from */ - private final HashMap, Set> providers = new HashMap<>(); - + private final HashSet> providers = + new HashSet<>(); + private AlbumProvider albums; + /** - * The collection of update listeners for each provider. - */ - private final HashMap, UpdateListener> updateListener = new HashMap<>(); - - /** - * A cache of all albums used. - */ - private final HashMap> cachedAlbums = new HashMap<>(); - - /** - * A cache of all album names. - */ - private final HashMap cachedAlbumNames = new HashMap<>(); - - /** - * A cache of all songs used. - */ - private final HashSet cachedSongs = new HashSet<>(); - - /** - * A cache of all song artists used. - */ - private final HashMap> cachedArtists = new HashMap<>(); - - /** - * A cache of all album artists used. - */ - private final HashMap> cachedAlbumArtists = new HashMap<>(); - - /** - * A cache of all genres used. - */ - private final HashMap> cachedGenres = new HashMap<>(); - - /** - * A cache of all years used. - */ - private final HashMap> cachedYears = new HashMap<>(); - - /** - * Creates a new CompiledSongProvider containing a set of existing providers. + * Creates a new CompiledSongProvider containing a set of existing + * providers. * * @param providers - Providers to add. */ - public CompiledSongProvider(SongProvider... providers) + public CompiledSongProvider(SongProvider... providers) { for (SongProvider provider : providers) { this.addProvider(provider); } } - + /** * Adds a provider to the list * * @param provider - The provider to add. */ - public void addProvider(SongProvider provider) + public void addProvider(SongProvider provider) { UpdateListener listener; - if (!this.providers.containsKey(provider)) + if (this.providers.add(provider)) { - this.providers.put(provider, new HashSet<>(provider.getSongs())); - listener = (song, totalSongs, updateText) -> { - /* - * Resets the cache. - */ - if (song == totalSongs || totalSongs == 0) - { - this.removeFromCache(this.providers.get(provider)); - this.addToCache(provider); - } - this.triggerUpdateListeners(); - }; - provider.addUpdateListener(listener); - this.addToCache(provider); + if (this.albums == null) + { + this.albums = provider.getAlbumProvider(); + } + provider.addUpdateListener(this); + triggerUpdateListeners(); } } - - /** - * Caches all the songs contained in a provider. - * - * @param provider - The provider to cache. - */ - private void addToCache(SongProvider 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); - } - } - } - + /** * Removes a provider from the compilation. * @@ -167,75 +66,38 @@ public class CompiledSongProvider implements SongProvider */ public void removeProvider(SongProvider provider) { - this.removeFromCache(this.providers.remove(provider)); - provider.removeUpdateListener(this.updateListener.remove(provider)); + provider.removeUpdateListener(this); + this.providers.remove(provider); } - - /** - * Removes all songs from a provider from a cache. - * - * @param songs - The songs to move out. - */ - private void removeFromCache(Set songs) + + @Override + public AlbumProvider getAlbumProvider() { - if (songs != null) + return this.albums; + } + + @Override + public void joinUpdate() throws InterruptedException + { + for (SongProvider provider: this.providers) { - 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) - { - this.cachedArtists.get(artist).remove(song); - if (this.cachedArtists.get(artist).isEmpty()) - { - this.cachedArtists.remove(artist); - } - } - 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); - } - } + provider.joinUpdate(); } } - + /** - * 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 - public Collection getAlbums() + public Set getCollection() { - return this.cachedAlbums.keySet(); + return this.providers.stream().map(SongProvider::getCollection) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); } - + /** * Obtains all songs within the collection. * @@ -244,9 +106,9 @@ public class CompiledSongProvider implements SongProvider @Override public Collection getSongs() { - return this.cachedSongs; + return this.getCollection(); } - + /** * Obtains a list of all artists. * @@ -255,149 +117,76 @@ public class CompiledSongProvider implements SongProvider @Override public Collection 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 getAlbumArtists() - { - return this.cachedAlbumArtists.keySet(); - } - - /** - * Obtains a list of all genres. - * - * @return All genres. - */ - @Override - public Collection getGenres() - { - return this.cachedGenres.keySet(); - } - - /** - * Obtains a list of all years that have albums. - * - * @return All years. - */ - @Override - public Collection getYears() - { - return this.cachedYears.keySet(); - } - + /** * 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. + * @return All songs from the requested album, or null if that album is not + * in the database. */ @Override public Collection 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. * * @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. + * @return A list of all songs from the specified artist, or null if that + * artist is not in the database. */ @Override public Collection 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 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 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 getAlbumsFromYear(int year) - { - return this.cachedYears.get(year); - } - + @Override public int getUpdateProgress() { int totalUpdate = 0; - for (SongProvider provider : this.providers.keySet()) + for (SongProvider provider : this.providers) { totalUpdate += provider.getUpdateProgress(); } return totalUpdate; } - + @Override - public int getTotalUpdateSongs() + public int getTotalUpdates() { int totalUpdate = 0; - for (SongProvider provider : this.providers.keySet()) + for (SongProvider provider : this.providers) { - if (provider.getTotalUpdateSongs() == -1) + if (provider.getTotalUpdates() == -1) { return -1; } else { - totalUpdate += provider.getTotalUpdateSongs(); + totalUpdate += provider.getTotalUpdates(); } } return totalUpdate; } - + @Override public String getUpdateText() { - for (SongProvider provider: this.providers.keySet()) + for (SongProvider provider : this.providers) { if (provider.getUpdateText() != null) { @@ -406,19 +195,19 @@ public class CompiledSongProvider implements SongProvider } return null; } - + @Override public void addUpdateListener(UpdateListener listener) { this.listeners.add(listener); } - + @Override public void removeUpdateListener(UpdateListener listener) { this.listeners.remove(listener); } - + /** * Triggers all update listeners. */ @@ -426,7 +215,23 @@ public class CompiledSongProvider implements SongProvider { 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 void onUpdate(DataProvider provider, int updated, int totalUpdate, String updating) + { + this.triggerUpdateListeners(); + } } diff --git a/interface/src/main/java/edu/regis/universeplayer/data/DataProvider.java b/interface/src/main/java/edu/regis/universeplayer/data/DataProvider.java new file mode 100644 index 0000000..f620827 --- /dev/null +++ b/interface/src/main/java/edu/regis/universeplayer/data/DataProvider.java @@ -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 +{ + /** + * 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 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); +} diff --git a/interface/src/main/java/edu/regis/universeplayer/data/DatabaseProvider.java b/interface/src/main/java/edu/regis/universeplayer/data/DatabaseProvider.java new file mode 100644 index 0000000..ec0747b --- /dev/null +++ b/interface/src/main/java/edu/regis/universeplayer/data/DatabaseProvider.java @@ -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 implements DataProvider +{ + 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 listeners = new LinkedList<>(); + + private final AtomicInteger progress = new AtomicInteger(0); + private final AtomicInteger updating = new AtomicInteger(0); + private String updateItem; + + private final HashSet 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} + *

+ * 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. + *

+ * + * @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. + *

+ * 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 serializeItem(T item); + + /** + * Adds an item to the database. + * + * @param item - The item to write. + */ + public final Future 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. + *

+ * Note that this method is called from the same thread that the scanner is + * from. + *

+ * + * @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 getCollection() + { + synchronized (this.collection) + { + return new HashSet<>(this.collection); + } + } + + private class WriterAction extends ForkJoinTask + { + private T item; + private Map 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 + { + 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; + } + } +} diff --git a/interface/src/main/java/edu/regis/universeplayer/data/DefaultAlbumProvider.java b/interface/src/main/java/edu/regis/universeplayer/data/DefaultAlbumProvider.java new file mode 100644 index 0000000..77b095e --- /dev/null +++ b/interface/src/main/java/edu/regis/universeplayer/data/DefaultAlbumProvider.java @@ -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 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 getAlbums() + { + return this.getCollection(); + } + + /** + * Obtains a list of all album artists. + * + * @return All album artists. + */ + @Override + public Collection getAlbumArtists() + { + return this.getCollection().stream().filter(a -> a.artists != null) + .map(a -> new HashSet<>(Arrays.asList(a.artists))) + .reduce(new HashSet<>(), (strings, strings2) -> { + HashSet comb = new HashSet<>(); + comb.addAll(strings); + comb.addAll(strings2); + return comb; + }); + } + + /** + * Obtains a list of all genres. + * + * @return All genres. + */ + @Override + public Collection getGenres() + { + return this.getCollection().stream().filter(a -> a.genres != null) + .map(a -> new HashSet<>(Arrays.asList(a.genres))) + .reduce(new HashSet<>(), (strings, strings2) -> { + HashSet 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 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 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 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 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. + *

+ * 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>) (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>) (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 serializeItem(Album item) + { + LinkedHashMap 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. + *

+ * Note that this method is called from the same thread that the scanner is + * from. + *

+ * + * @return A fork-join task to invoke. This may be null. + */ + @Override + protected ForkJoinTask[] onComplete() + { + return null; + } +} diff --git a/interface/src/main/java/edu/regis/universeplayer/data/InternetSongProvider.java b/interface/src/main/java/edu/regis/universeplayer/data/InternetSongProvider.java index e7108bc..f951228 100644 --- a/interface/src/main/java/edu/regis/universeplayer/data/InternetSongProvider.java +++ b/interface/src/main/java/edu/regis/universeplayer/data/InternetSongProvider.java @@ -7,657 +7,296 @@ package edu.regis.universeplayer.data; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.File; +import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.*; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ForkJoinTask; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; -public class InternetSongProvider implements SongProvider +import edu.regis.universeplayer.browser.Browser; +import edu.regis.universeplayer.browserCommands.QuerySongData; + +public class InternetSongProvider extends DatabaseProvider implements SongProvider { private static final Logger logger = LoggerFactory.getLogger(InternetSongProvider.class); - private static final ExecutorService service = Executors.newSingleThreadExecutor(); + 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() { - if (INSTANCE == null) - { - INSTANCE = new InternetSongProvider(); - } return INSTANCE; } - private AtomicBoolean updating = new AtomicBoolean(); - - private final HashMap songs = new HashMap<>(); - private final HashMap albums = new HashMap<>(); - /** - * A cache of all song artists. - */ - private final HashSet artists = new HashSet<>(); - /** - * A cache of all album genres. - */ - private final HashSet genres = new HashSet<>(); - /** - * A cache of all album artists. - */ - private final HashSet albumArtists = new HashSet<>(); - /** - * A cache of all album release years. - */ - private final HashSet years = new HashSet<>(); - - private int updatedSongs; - private int totalUpdate; private final LinkedList 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 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 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 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 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 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 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 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 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 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 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 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 public int getUpdateProgress() { - return this.updatedSongs; + int sup = super.getUpdateProgress(); + if (sup == 0) + { + sup = this.progress.get(); + } + return sup; } - + @Override - public int getTotalUpdateSongs() + public int getTotalUpdates() { - return this.totalUpdate; + int sup = super.getTotalUpdates(); + if (sup == 0) + { + sup = this.updating.get(); + } + return sup; } - + @Override public String getUpdateText() { - return ""; - } - - @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.getUpdateProgress(), this.getTotalUpdateSongs(), this.getUpdateText())); - } - - public Future 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(); - song.location = 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() + String sup = super.getUpdateText(); + if (sup == null || sup.isEmpty()) { + sup = this.updateItem; } - - @Override - public void run() - { - Statement state; - ResultSet result; - Album album; - InternetSong song; - int numAlbums = 0, numSongs = 0; + return sup; + } - synchronized (updating) + /** + * 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 + public AlbumProvider getAlbumProvider() + { + return this.albums; + } + + /** + * Called when an entry is read from the database and is ready to be + * parsed. + *

+ * 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 InternetSong readResult(ResultSet result) throws SQLException + { + InternetSong song = new InternetSong(); + song.location = result.getURL("url"); + song.title = result.getString("title"); + song.artists = + Arrays.stream(result.getString("artists").split(";")) + .map(String::trim).toArray(String[]::new); + song.trackNum = result.getInt("track"); + song.disc = result.getInt("disc"); + song.duration = result.getLong("duration"); + try + { + getAlbumProvider().joinUpdate(); + song.album = getAlbumProvider().getAlbumByName(result.getString( + "album")); + } + catch (InterruptedException e) + { + logger.error("Couldn't wait for album provider for song {}", song + , e); + } + 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 serializeItem(InternetSong item) + { + LinkedHashMap 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. + *

+ * Note that this method is called from the same thread that the scanner is + * from. + *

+ * + * @return A fork-join task to invoke. This may be null. + */ + @Override + protected ForkJoinTask[] onComplete() + { + return null; + } + + public ForkJoinTask addSong(URL url) + { + AddInternetTask task = new AddInternetTask(url); + this.service.execute(task); + return task; + } + + private class AddInternetTask extends ForkJoinTask + { + 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) { - 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.artists = Optional - .ofNullable(result.getString("artists")) - .map(s -> s.split(";")) - .orElse(new String[0]); - song.trackNum = result.getInt("track"); - song.disc = result.getInt("disc"); - song.duration = result.getLong("duration"); - song.album = Optional - .ofNullable(result.getString("album")) - .map(albums::get) - .orElse(albums.get("Unknown")); - numSongs++; - } - } - state.close(); - } + writeItem(data).get(); } - catch (SQLException e) + catch (InterruptedException | ExecutionException e) { - logger.error("Could not query SQL database.", e); + this.completeExceptionally(e); + return false; } - logger.debug("Query complete, retrieved {} albums and {} songs", numAlbums, numSongs); - updatedSongs = 0; - totalUpdate = 0; - updating.set(false); - updating.notifyAll(); } - triggerUpdateListeners(); + this.complete(data); + return true; } } } diff --git a/interface/src/main/java/edu/regis/universeplayer/data/LocalSongProvider.java b/interface/src/main/java/edu/regis/universeplayer/data/LocalSongProvider.java index 9d0e623..637dd4a 100644 --- a/interface/src/main/java/edu/regis/universeplayer/data/LocalSongProvider.java +++ b/interface/src/main/java/edu/regis/universeplayer/data/LocalSongProvider.java @@ -9,49 +9,37 @@ import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.sql.*; import java.util.*; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ForkJoinTask; +import java.util.concurrent.Future; +import java.util.concurrent.RecursiveAction; +import java.util.concurrent.RecursiveTask; +import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.Stream; -public class LocalSongProvider implements SongProvider +import edu.regis.universeplayer.ConfigManager; + +public class LocalSongProvider extends DatabaseProvider implements SongProvider { private static final Logger logger = LoggerFactory .getLogger(LocalSongProvider.class); + private static final ResourceBundle langs = ResourceBundle + .getBundle("lang.interface", Locale.getDefault()); private static final HashSet formats = new HashSet<>(); private static final HashSet codecs = new HashSet<>(); - private static final ForkJoinPool service = new ForkJoinPool(); - private static String currentFolder; + private final AlbumProvider albums; - private final File source; - - private final AtomicBoolean updating = new AtomicBoolean(false); - private final HashMap songs = new HashMap<>(); - private final HashMap albums = new HashMap<>(); - /** - * A cache of all song artists. - */ - private final HashSet artists = new HashSet<>(); - /** - * A cache of all album genres. - */ - private final HashSet genres = new HashSet<>(); - /** - * A cache of all album artists. - */ - private final HashSet albumArtists = new HashSet<>(); - /** - * A cache of all album release years. - */ - private final HashSet years = new HashSet<>(); - - private int updatedSongs; - private int totalUpdate; - private final LinkedList listeners = new LinkedList<>(); + private final AtomicInteger progress = new AtomicInteger(0); + private final AtomicInteger updating = new AtomicInteger(0); + private String updateItem; /** * Obtains all formats supported by FFMPEG. Note that this list includes @@ -62,7 +50,7 @@ public class LocalSongProvider implements SongProvider public static Set getFormats() { final Pattern FILEPAT = Pattern - .compile("^\\s*[D ][E ]\\s*([a-z1-9_]{2,}(,[a-z_]{2,})*)\\s*[A-Za-z1-9 \\(\\)-/'\\.\":]+$"); + .compile("^\\s*[D ][E ]\\s*([a-z1-9_]{2,}(,[a-z_]{2,})*)\\s*[A-Za-z1-9 ()-/'.\":]+$"); final Pattern FILEPAT2 = Pattern .compile("[a-z1-9_]{2,}(,[a-z1-9_]{2,})*"); Matcher matcher; @@ -163,1172 +151,713 @@ public class LocalSongProvider implements SongProvider return codecs; } - public LocalSongProvider(File source) + public LocalSongProvider(AlbumProvider albums) { - Connection dbL = null; - this.source = source; - if (this.source == null || !this.source.isDirectory()) - { - throw new IllegalArgumentException("File source must be existing directory"); - } - getSongCache(); - } - - private void getSongCache() - { - updating.set(true); - service.submit(new SongQuery(true)); - } - - /** - * Obtains all albums within the collection. - * - * @return A list of albums. - */ - @Override - public Collection 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 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 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 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 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 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 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 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 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 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 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()); - } + this.albums = albums; } @Override public int getUpdateProgress() { - return this.updatedSongs; + int sup = super.getUpdateProgress(); + if (sup == 0) + { + sup = this.progress.get(); + } + return sup; } @Override - public int getTotalUpdateSongs() + public int getTotalUpdates() { - return this.totalUpdate; + int sup = super.getTotalUpdates(); + if (sup == 0) + { + sup = this.updating.get(); + } + return sup; } @Override public String getUpdateText() { - return currentFolder; + String sup = super.getUpdateText(); + if (sup == null || sup.isEmpty()) + { + if (this.updateItem != null) + { + sup = new Formatter().format(langs.getString("update" + + ".local"), this.updateItem).toString(); + } + } + return sup; } @Override - public void addUpdateListener(UpdateListener listener) + public AlbumProvider getAlbumProvider() { - this.listeners.add(listener); + return this.albums; } + /** + * Obtains the name of + * + * @return The name of the database table. This is case insensitive. + */ @Override - public void removeUpdateListener(UpdateListener listener) + protected String getDatabaseTable() { - this.listeners.remove(listener); + return "local_songs"; } - protected void triggerUpdateListeners() + /** + * 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() { - this.listeners.forEach(listener -> listener - .onUpdate(this.getUpdateProgress(), this - .getTotalUpdateSongs(), this.getUpdateText())); + /* + * Create the table + */ + return "CREATE TABLE local_songs" + + "(file TEXT PRIMARY KEY NOT NULL," + + "codec CHAR(5)," + + "type CHAR(5)," + + "title TEXT," + + "artists TEXT," + + "track INTEGER," + + "disc INTEGER," + + "duration BIGINT," + + "album TEXT," + + "mod BIGINT);"; } - private class SongScanner extends RecursiveAction + /** + * Called when an entry is read from the database and is ready to be + * parsed. + *

+ * 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 LocalSong readResult(ResultSet result) throws SQLException { - private final File file; + LocalSong song = new LocalSong(); + song.file = new File(result.getString("file")); + song.codec = result.getString("codec"); + song.type = result.getString("type"); + song.title = result.getString("title"); + song.artists = + Arrays.stream(result.getString("artists").split(";")) + .dropWhile(String::isEmpty) + .map(String::trim).toArray(String[]::new); + song.trackNum = result.getInt("track"); + song.disc = result.getInt("disc"); + song.duration = result.getLong("duration"); + try + { + getAlbumProvider().joinUpdate(); + song.album = getAlbumProvider().getAlbumByName(result.getString( + "album")); + } + catch (InterruptedException e) + { + logger.error("Couldn't wait for album update for song {}", song, e); + } + song.lastMod = result.getLong("mod"); + return song; + } - SongScanner(File folder) + /** + * Called to obtain the properties of an object to write. + * + * @param item - The item to serialize. + * @return Properties to write. + */ + @Override + protected Map serializeItem(LocalSong item) + { + LinkedHashMap map = new LinkedHashMap<>(); + map.put("file", item.file.getAbsolutePath()); + map.put("codec", item.codec); + map.put("type", item.type); + map.put("title", item.title); + map.put("artists", Arrays.stream(item.artists).reduce("", + (s1, s2) -> s1.isEmpty() ? 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)); + map.put("mod", item.lastMod); + 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(LocalSong data) + { + return Optional.ofNullable(data.album).map(a -> a.name).orElse(null) + + "/" + data.title; + } + + /** + * A callback for when the database scan is complete. + *

+ * Note that this method is called from the same thread that the scanner is + * from. + *

+ * + * @return A fork-join task to invoke. This may be null. + */ + @Override + protected ForkJoinTask[] onComplete() + { + LinkedHashMap, ArrayList>> scanRoots = + new LinkedHashMap<>(); + boolean existing; + Map.Entry, ArrayList> subPaths; + logger.debug("Searching for scan roots among {}", (Object) ConfigManager + .getMusicDirs()); + for (Path toScan : ConfigManager.getMusicDirs()) + { + existing = false; + /* + * Make sure that some scan roots aren't subfolders of other roots. + */ + for (Path existingPath : scanRoots.keySet()) + { + if (toScan.startsWith(existingPath)) + { + scanRoots.get(existingPath).getKey().add(toScan); + existing = true; + break; + } + } + if (!existing) + { + /* + * Give each scanner a list of excluded folders that they + * will run across. + */ + subPaths = new AbstractMap.SimpleEntry<>(new ArrayList<>(), + new ArrayList<>()); + for (Path toExclude : ConfigManager.getMusicExcludeDirs()) + { + if (toScan.startsWith(toExclude)) + { + subPaths.getValue().add(toExclude); + /* + * While we could ensure that we exclude subfolders + * of other excludes, there shouldn't be any + * performance impact from not doing so, and doing so + * would waste processing cycles. + */ + } + } + scanRoots.put(toScan, subPaths); + } + } + ArrayList toScan = new ArrayList<>(); + List>> tasks = + scanRoots.entrySet().stream() + .map(entry -> new FolderCounter(entry.getKey(), + entry.getValue().getKey().toArray(Path[]::new), + entry.getValue().getValue() + .toArray(Path[]::new))) + .map(this.service::submit) + .collect(Collectors.toList()); + this.updating.set(-1); + for (ForkJoinTask> task : tasks) + { + toScan.addAll(task.join()); + } + this.updating.set(toScan.size()); + logger.debug("Scanning {} files", toScan.size()); + return toScan.stream().map(SongScanner::new) + .toArray(SongScanner[]::new); + } + + private class FolderCounter extends RecursiveTask> + { + private final Path source; + private final Path[] exclude; + private final Path[] include; + + public FolderCounter(Path source, Path[] exclude, Path[] include) + { + this.source = source; + this.exclude = exclude; + this.include = include; + } + + /** + * The main computation performed by this task. + * + * @return the result of the computation + */ + @Override + protected List compute() + { + List results = new ArrayList<>(); + if (Files.isDirectory(this.source)) + { + try + { + List tasks = + Files.list(this.source).flatMap(f -> { + ArrayList toInclude = new ArrayList<>(); + if (Arrays.binarySearch(this.exclude, f) > -1) + { + /* + * Just skip + */ + for (Path include : this.include) + { + if (include.startsWith(f)) + { + toInclude.add(include); + } + } + } + else + { + toInclude.add(f); + } + return toInclude.stream(); + }).filter(f -> { + if (Files.isDirectory(f)) + { + return true; + } + else + { + results.add(f); + return false; + } + }).map(f -> { + Path[] exclude = + Arrays.stream(this.exclude) + .filter(e -> e.startsWith(f)) + .toArray(Path[]::new); + Path[] include = + Arrays.stream(this.include) + .filter(e -> e.startsWith(f)) + .toArray(Path[]::new); + return new FolderCounter(f, exclude, include); + }).collect(Collectors.toList()); + results.addAll(invokeAll(tasks).stream().flatMap(s -> { + try + { + return s.get().stream(); + } + catch (InterruptedException | ExecutionException e) + { + logger.error("Could not get results for {}", s, e); + return Stream.empty(); + } + }).collect(Collectors.toList())); + } + catch (IOException e) + { + logger.error("Could not get subfolders of {}", this.source, + e); + } + } + return results; + } + } + + private class SongScanner extends ForkJoinTask + { + private final Path file; + private LocalSong song; + + SongScanner(Path folder) { this.file = folder; } + /** + * 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 void compute() + public LocalSong getRawResult() + { + return this.song; + } + + /** + * 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(LocalSong value) + { + this.song = value; + } + + @Override + public boolean exec() + { + LocalSong existing; + boolean update; + + existing = + getCollection().stream() + .filter(f -> f.file.toPath() + .equals(this.file)) + .findFirst().orElse(null); + if (existing != null) + { + /* + * The file has been updated. + */ + update = existing.file.lastModified() > existing.lastMod; + } + else + { + update = true; + } + if (update) + { + updateItem = + this.file.subpath(this.file.getNameCount() - 3, + this.file.getNameCount() - 1).toString(); + triggerUpdateListeners(); + try + { + this.complete(this.readSong(this.file, existing)); + } + catch (IOException | InterruptedException e) + { + this.completeExceptionally(e); + } + } + progress.incrementAndGet(); + triggerUpdateListeners(); + return this.isCompletedNormally(); + } + + private LocalSong readSong(Path path, LocalSong write) throws IOException, InterruptedException { Process process; String line, lineData; String[] streamData; - + Album album; String type; - String codec; - - String genre = null; + String codec = null; + String[] genre = null; String title = null; - String artist = null; + String[] artist = null; String albumTitle = null; - String albumArtist = null; + String[] albumArtist = null; + int year = 0; long duration = 0; Integer[] track = null; Integer[] disc = null; - Statement state = null; - ResultSet result; + type = path.getFileName().toString() + .substring(path.getFileName().toString() + .lastIndexOf('.') + 1).toLowerCase(); - try + if (!getFormats().contains(type)) { - if (file.getName().lastIndexOf(".") < file.getName() - .length() - 1) + return null; + } + + process = Runtime.getRuntime() + .exec(new String[]{"ffprobe", "-hide_banner", + path.toAbsolutePath().toString()}); + process.waitFor(); + + try (Scanner scanner = new Scanner(process.getErrorStream())) + { + while (scanner.hasNextLine()) { - type = file.getName() - .substring(file.getName().lastIndexOf('.') + 1) - .toLowerCase(); - if (getFormats().contains(type)) + line = scanner.nextLine().trim(); + try { - try + final String data; + int index = line.indexOf(':'); + if (index > 0 && index < line.length() - 2) { - synchronized (DatabaseManager.getDb()) + data = line.substring(index + 2); + } + else + { + data = line; + } + switch (line.toLowerCase() + .substring(0, line + .indexOf(' ') > 0 ? line + .indexOf(' ') : line + .length())) + { + case "genre" -> { + /* + * We only take the first one, as to + * avoid mishaps with labels after the + * metadata. + */ + if (genre == null) { - state = DatabaseManager.getDb() - .createStatement(); - result = state - .executeQuery("SELECT mod FROM local_songs WHERE file='" + this.file - .getAbsolutePath() - .replaceAll("'", "''") + "';"); - if (result.next()) - { - if (result.getLong(1) >= this.file - .lastModified()) - { - /* - * No modifications needed - */ - logger.debug("No update needed for {}", file); - updatedSongs++; - triggerUpdateListeners(); - return; - } - else - { - state.executeUpdate("UPDATE local_songs SET mod = " + this.file - .lastModified() + " WHERE file='" + this.file - .getAbsolutePath() - .replaceAll("'", "''") + "';"); - } - } + genre = Arrays.stream(data.split(";")) + .map(String::trim) + .toArray(String[]::new); } - currentFolder = file.getPath(); - codec = null; - process = Runtime.getRuntime() - .exec(new String[]{"ffprobe", "-hide_banner", file.getAbsolutePath()}); - process.waitFor(); - try (Scanner scanner = new Scanner(process - .getErrorStream())) + } + case "title" -> { + if (title == null) { - int i = 0; - while (scanner.hasNextLine()) - { - line = scanner.nextLine().trim(); - try - { - switch (line.toLowerCase() - .substring(0, line - .indexOf(' ') > 0 ? line - .indexOf(' ') : line - .length())) - { - case "genre" -> { - /* - * We only take the first one, as to - * avoid mishaps with labels after the - * metadata. - */ - if (genre == null) - { - genre = line.substring(line - .indexOf(':') + 2); - } - } - case "title" -> { - if (title == null) - { - title = line.substring(line - .indexOf(':') + 2); - } - } - case "artist" -> { - if (artist == null) - { - artist = line.substring(line - .indexOf(':') + 2); - } - } - case "album" -> { - if (albumTitle == null) - { - albumTitle = line.substring(line - .indexOf(':') + 2); - } - } - case "album_artist" -> { - if (albumArtist == null) - { - albumArtist = line - .substring(line - .indexOf(':') + 2); - } - } - case "track" -> { - lineData = line.substring(line - .indexOf(':') + 2); - if (lineData.indexOf('/') >= 0) - { - track = Arrays - .stream(lineData - .split("/")) - .map(Integer::parseInt) - .toArray(Integer[]::new); - } - else - { - if (track != null) - { - track[0] = Integer - .parseInt(lineData); - } - else - { - track = new Integer[]{Integer.parseInt(lineData), -1}; - } - } - } - case "tracktotal" -> { - if (track != null) - { - track[1] = Integer.parseInt(line - .substring(line - .indexOf(':') + 2)); - } - else - { - track = new Integer[]{-1, Integer.parseInt(line - .substring(line - .indexOf(':') + 2))}; - } - } - case "disc" -> { - lineData = line.substring(line - .indexOf(':') + 2); - if (lineData.indexOf('/') >= 0) - { - disc = Arrays - .stream(lineData - .split("/")) - .map(Integer::parseInt) - .toArray(Integer[]::new); - } - else - { - if (disc != null) - { - disc[0] = Integer - .parseInt(lineData); - } - else - { - disc = new Integer[]{Integer.parseInt(lineData), -1}; - } - } - } - case "disctotal" -> { - if (disc != null) - { - disc[1] = Integer.parseInt(line - .substring(line - .indexOf(':') + 2)); - } - else - { - disc = new Integer[]{-1, Integer.parseInt(line - .substring(line - .indexOf(':') + 2))}; - } - } - case "duration:" -> { - if (duration == 0) - { - lineData = line.substring(line - .indexOf(':') + 2, line - .indexOf(',')); - if (!lineData.equals( - "N/A")) - { - duration = Long - .parseLong(lineData - .substring(0, 2)) * 3600 * 1000 + Long - .parseLong(lineData - .substring(3, 5)) * 60 * 1000 + Long - .parseLong(lineData - .substring(6, 8)) * 1000 + (long) (Float - .parseFloat(lineData - .substring(8, lineData - .length() - 1)) * 1000); - } - } - } - case "stream" -> { - streamData = line.split(" "); - if (streamData[2].equals("Audio:")) - { - codec = streamData[3]; - if (codec.endsWith(",")) - { - codec = codec - .substring(0, codec - .length() - 1); - } - /* - * If this isn't a supported codec, - * discard. - */ - if (!getCodecs() - .contains(codec)) - { - logger.trace("Invalid codec {} for song {}", codec, file); - codec = null; - } - else - { - logger.trace("Found codec {} for song {}", codec, file); - } - } - else - { - logger.trace("Found non-audio stream {} for {}", line, file); - } - } - } - } - catch (NumberFormatException e) - { - throw new RuntimeException( - "Could not parse line \"" + line + "\"", e); - } - } -// logger.trace("Finished scanning {}", file); + title = data; } - if (codec != null) + } + case "artist" -> { + if (artist == null) { - /* - * Update album information. - */ - synchronized (DatabaseManager.getDb()) - { - /* - * This part in particular is prone to thread-safety issues. - */ - if (albumTitle != null) - { - albumTitle = albumTitle.replaceAll("'", - "''"); - } - result = state - .executeQuery("SELECT album FROM local_albums WHERE album='" + albumTitle + "';"); - if (!result.next()) - { - state.executeUpdate("INSERT INTO local_albums (album) VALUES ('" + albumTitle + "');"); - } - result = state - .executeQuery("SELECT artists FROM local_albums WHERE album='" + albumTitle + "';"); - if (result - .getString("artists") == null && albumArtist != null) - { - state.executeUpdate("UPDATE " + - "local_albums SET artists='" + Arrays - .stream(albumArtist.split(";")) - .map(String::trim).map(s -> s - .replaceAll("'", "''")) - .collect(Collectors - .joining(";")) + "' WHERE album='" + albumTitle + "';"); - } - // TODO - Can we get year metadata? - result = state - .executeQuery("SELECT genres FROM local_albums WHERE album='" + albumTitle + "';"); - if (result - .getString("genres") == null && genre != null) - { - state.executeUpdate("UPDATE local_albums SET genres='" + Arrays - .stream(genre.split(";")) - .map(String::trim).map(s -> s - .replaceAll("'", "''")) - .collect(Collectors - .joining(";")) + "' WHERE album='" + albumTitle + "';"); - } - result = state - .executeQuery("SELECT tracks FROM local_albums WHERE album='" + albumTitle + "';"); - if (result - .getInt("tracks") == 0 && track != null && track[1] > 0) - { - state.executeUpdate("UPDATE local_albums SET tracks=" + track[1] + " WHERE album='" + albumTitle + "';"); - } - result = state - .executeQuery("SELECT discs FROM local_albums WHERE album='" + albumTitle + "';"); - if (result - .getInt("discs") == 0 && disc != null && disc[1] > 0) - { - state.executeUpdate("UPDATE local_albums SET tracks=" + disc[1] + " WHERE album='" + albumTitle + "';"); - } - - /* - * Create the song - */ - result = state - .executeQuery("SELECT title FROM local_songs WHERE file='" + file - .getAbsolutePath() - .replaceAll("'", "''") + "';"); - if (result.next()) - { - logger.debug("Updating song cache for {} ({})", title, file); - StringBuilder sql = new StringBuilder("UPDATE local_songs SET "); - sql.append("codec='").append(codec) - .append("', "); - sql.append("type='").append(codec) - .append("', "); - if (title != null && !title.isEmpty()) - { - sql.append("title='").append(title - .replaceAll("'", "''")) - .append("', "); - } - else - { - sql.append("title='") - .append(file.getName() - .replaceAll("'", "''")) - .append("', "); - } - if (artist != null && !artist.isEmpty()) - { - sql.append("artists='") - .append(Optional.of(artist) - .map(s -> s - .split(";")) - .stream() - .flatMap(Arrays::stream) - .map(String::trim).map(s -> s.replaceAll("'", "''")) - .collect(Collectors - .joining(";"))) - .append("', "); - } - else - { - sql.append("artists=NULL, "); - } - if (track != null && track[0] > 0) - { - sql.append("track=") - .append(track[0]).append(", "); - } - else - { - sql.append("track=NULL, "); - } - if (disc != null && disc[0] > 0) - { - sql.append("disc=").append(disc[0]) - .append(", "); - } - else - { - sql.append("disc=NULL, "); - } - if (duration != 0) - { - sql.append("duration=") - .append(duration).append(", "); - } - else - { - sql.append("duration=NULL, "); - } - if (albumTitle != null && !albumTitle - .isEmpty()) - { - sql.append("album='") - .append(albumTitle) - .append("', "); - } - else - { - sql.append("album=NULL, "); - } - sql.append("mod=") - .append(file.lastModified()); - sql.append(" WHERE file='") - .append(file.getAbsolutePath() - .replaceAll("'", "''")) - .append("';"); - state.executeUpdate(sql.toString()); - } - else - { - logger.debug("Caching song {} ({})", title, file); - StringBuilder sql = new StringBuilder("INSERT INTO local_songs "); - StringBuilder columns = new StringBuilder("("); - StringBuilder values = new StringBuilder("("); - - columns.append("file,"); - values.append('\'') - .append(file.getAbsolutePath() - .replaceAll("'", "''")) - .append("',"); - columns.append("codec,"); - values.append('\'').append(codec) - .append("',"); - columns.append("type,"); - values.append('\'').append(type) - .append("',"); - if (title != null && !title.isEmpty()) - { - columns.append("title,"); - values.append('\'').append(title - .replaceAll("'", "''")) - .append("',"); - } - if (artist != null && !artist.isEmpty()) - { - columns.append("artists,"); - values.append('\'') - .append(Optional.of(artist) - .map(s -> s - .split(";")) - .stream() - .flatMap(Arrays::stream) - .map(String::trim) - .map(s -> s - .replaceAll("'", "''")) - .collect(Collectors - .joining(";"))) - .append("',"); - } - if (track != null && track[0] > 0) - { - columns.append("track,"); - values.append(track[0]).append(","); - } - if (disc != null && disc[0] > 0) - { - columns.append("disc,"); - values.append(disc[0]).append(","); - } - if (duration > 0) - { - columns.append("duration,"); - values.append(duration).append(","); - } - if (albumTitle != null && !albumTitle - .isEmpty()) - { - columns.append("album,"); - values.append('\'') - .append(albumTitle) - .append("',"); - } - - columns.append("mod"); - values.append(file.lastModified()); - - columns.append(") VALUES "); - values.append(");"); - sql.append(columns); - sql.append(values); - state.executeUpdate(sql.toString()); - } - } - - updatedSongs++; - triggerUpdateListeners(); + artist = Arrays.stream(data.split(";")) + .map(String::trim) + .toArray(String[]::new); + } + } + case "album" -> { + if (albumTitle == null) + { + albumTitle = data; + } + } + case "album_artist" -> { + if (albumArtist == null) + { + albumArtist = Arrays.stream(data.split(";")) + .map(String::trim) + .toArray(String[]::new); + } + } + case "track" -> { + lineData = data; + if (lineData.indexOf('/') >= 0) + { + track = Arrays + .stream(lineData + .split("/")) + .map(Integer::parseInt) + .toArray(Integer[]::new); } else { + if (track != null) + { + track[0] = Integer + .parseInt(lineData); + } + else + { + track = new Integer[]{Integer.parseInt(lineData), -1}; + } + } + } + case "tracktotal" -> { + if (track != null) + { + track[1] = Integer.parseInt(data); + } + else + { + track = new Integer[]{-1, Integer.parseInt(data)}; + } + } + case "disc" -> { + lineData = data; + if (lineData.indexOf('/') >= 0) + { + disc = Arrays + .stream(lineData + .split("/")) + .map(Integer::parseInt) + .toArray(Integer[]::new); + } + else + { + if (disc != null) + { + disc[0] = Integer + .parseInt(lineData); + } + else + { + disc = new Integer[]{Integer.parseInt(lineData), -1}; + } + } + } + case "disctotal" -> { + if (disc != null) + { + disc[1] = Integer.parseInt(data); + } + else + { + disc = new Integer[]{-1, Integer.parseInt(data)}; + } + } + case "date" -> { + lineData = line.substring(line + .indexOf(':') + 2, + Math.min(line.indexOf(':') + 6, + line.length())).trim(); + year = Integer.parseInt(lineData); + } + case "duration:" -> { + if (duration == 0) + { + lineData = line.substring(line + .indexOf(':') + 2, line + .indexOf(',')); + if (!lineData.equals( + "N/A")) + { + duration = Long + .parseLong(lineData + .substring(0, 2)) * 3600 * 1000 + Long + .parseLong(lineData + .substring(3, 5)) * 60 * 1000 + Long + .parseLong(lineData + .substring(6, 8)) * 1000 + (long) (Float + .parseFloat(lineData + .substring(8, lineData + .length() - 1)) * 1000); + } + } + } + case "stream" -> { + streamData = line.split(" "); + if (streamData[2].equals("Audio:")) + { + codec = streamData[3]; + if (codec.endsWith(",")) + { + codec = codec + .substring(0, codec + .length() - 1); + } /* - * Never mind, this isn't an updatable song. + * If this isn't a supported codec, + * discard. */ - totalUpdate--; - triggerUpdateListeners(); - logger.trace("Could not find codec for {}", file); + if (!getCodecs() + .contains(codec)) + { + logger.trace("Invalid codec {} for song {}", codec, file); + codec = null; + } + else + { + logger.trace("Found codec {} for song {}", codec, file); + } } - } - catch (SQLException e) - { - logger.error("Could not write song " + file + " to database", e); - } - catch (IOException | InterruptedException e) - { - logger.error("Could not get ffprobe information on " + file, e); - } - finally - { - state.close(); - } - } - else - { - /* - * Never mind, this isn't an updatable song. - */ - totalUpdate--; - triggerUpdateListeners(); -// logger.trace("Scanned {}, not applicable", file); - } - } - else - { - /* - * Never mind, this isn't an updatable song. - */ - totalUpdate--; - triggerUpdateListeners(); -// logger.trace("Scanned {}, not applicable", file); - } - } - catch (Throwable e) - { - logger.error("Error in obtaining song " + this.file, e); - } - } - } - - private class SongQuery extends RecursiveAction - { - private final boolean scan; - - SongQuery(boolean scan) - { - this.scan = scan; - } - - @Override - protected void compute() - { - Statement state; - ResultSet result; - Album album; - LocalSong song; - int numAlbums = 0, numSongs = 0; - - updating.set(true); - synchronized (updating) - { - 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='LOCAL_ALBUMS';"); - if (!result.next()) - { - logger.debug("Creating album table."); - /* - * Create the table - */ - state.executeUpdate("CREATE TABLE LOCAL_ALBUMS" + - "(ALBUM TEXT PRIMARY KEY NOT NULL," + - "ARTISTS TEXT," + - "YEAR INTEGER," + - "GENRES TEXT," + - "TRACKS INTEGER," + - "DISCS INTEGER);"); - } - else - { - result = state - .executeQuery("SELECT * FROM LOCAL_ALBUMS;"); - while (result.next()) + else { - 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++; + logger.trace("Found non-audio stream {} for {}", line, file); } } - result = state - .executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='LOCAL_SONGS';"); - if (!result.next()) - { - logger.debug("Creating song table."); - /* - * Create the table - */ - state.executeUpdate("CREATE TABLE LOCAL_SONGS" + - "(FILE TEXT PRIMARY KEY NOT NULL," + - "CODEC CHAR(5)," + - "TYPE CHAR(5)," + - "TITLE TEXT," + - "ARTISTS TEXT," + - "TRACK INTEGER," + - "DISC INTEGER," + - "DURATION BIGINT," + - "ALBUM TEXT," + - "MOD BIGINT);"); } - else - { - result = state - .executeQuery("SELECT * FROM LOCAL_SONGS;"); - while (result.next()) - { - if (result.getString("file") == null) - { - continue; - } - song = songs - .get(new File(result - .getString("file"))); - if (song == null) - { - song = new LocalSong(); - song.file = new File(result - .getString("file")); - songs.put(song.file, song); - } - song.codec = result.getString("codec"); - song.type = result.getString("type"); - song.title = result.getString("title"); - song.artists = Optional - .ofNullable(result.getString("artists")) - .map(s -> s.split(";")) - .orElse(new String[0]); - song.trackNum = result.getInt("track"); - song.disc = result.getInt("disc"); - song.duration = result.getLong("duration"); - song.album = Optional - .ofNullable(result.getString("album")) - .map(albums::get) - .orElse(albums.get("Unknown")); - numSongs++; - } - } - state.close(); + } + catch (NumberFormatException e) + { + throw new RuntimeException( + "Could not parse line \"" + line + "\"", e); } } - catch (SQLException e) - { - logger.error("Could not query SQL database.", e); - } - logger.debug("Query complete, retrieved {} albums and {} songs", numAlbums, numSongs); - updatedSongs = 0; - totalUpdate = 0; + } - updating.set(false); - updating.notifyAll(); - } - triggerUpdateListeners(); - if (scan) + if (codec == null) { - LinkedList scanners = new LinkedList<>(); - this.invokeFolder(source, scanners); - triggerUpdateListeners(); - logger.debug("Scanning for changes in {} files...", totalUpdate); - invokeAll(scanners.toArray(SongScanner[]::new)); - invokeAll(new ScanCompletion()); + return null; } - } - /** - * Searches for all files and scans them - * - * @param dir - The file to scan - * @param scanners - The list to add the scanners to - */ - private void invokeFolder(File dir, List scanners) - { - if (dir.isDirectory()) + if (write == null) { - for (File file : Objects - .requireNonNullElse(dir.listFiles(), new File[0])) - { - this.invokeFolder(file, scanners); - } + write = new LocalSong(); + write.file = path.toFile(); + write.codec = codec; + write.type = type; } - else - { - totalUpdate++; - scanners.add(new SongScanner(dir)); - } - } - } - private class ScanCompletion extends RecursiveAction - { - @Override - protected void compute() - { - while (true) + write.lastMod = Files.getLastModifiedTime(path).toMillis(); + + write.title = title; + if (artist != null && artist.length > 0) { - if (service.awaitQuiescence(60, TimeUnit.SECONDS)) - { - break; - } + write.artists = artist; } - logger.debug("Scan complete. Researching database"); - currentFolder = ""; - updatedSongs = 0; - totalUpdate = 0; - triggerUpdateListeners(); - invokeAll(new SongQuery(false)); + if (duration > 0) + { + write.duration = duration; + } + if (track != null && track.length > 0) + { + write.trackNum = track[0]; + } + if (disc != null && disc.length > 0) + { + write.disc = disc[0]; + } + + getAlbumProvider().joinUpdate(); + album = albums.getAlbumByName(albumTitle); + if (album == null) + { + album = new Album(); + album.name = albumTitle; + } + if (albumArtist != null && albumArtist.length > 0) + { + album.artists = albumArtist; + } + if (genre != null && genre.length > 0) + { + album.genres = genre; + } + if (year > 0) + { + album.year = year; + } + if (track != null && track.length > 1) + { + album.totalTracks = track[1]; + } + if (disc != null && disc.length > 1) + { + album.totalDiscs = disc[1]; + } + write.album = album; + + getAlbumProvider().writeItem(album); + writeItem(write); + + return write; } } } diff --git a/interface/src/main/java/edu/regis/universeplayer/data/SimpleSongProvider.java b/interface/src/main/java/edu/regis/universeplayer/data/SimpleSongProvider.java deleted file mode 100644 index 1c40ad8..0000000 --- a/interface/src/main/java/edu/regis/universeplayer/data/SimpleSongProvider.java +++ /dev/null @@ -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 -{ - private ArrayList albums; - private ArrayList songs; - - @Override - public Collection 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 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 getArtists() - { - return this.songs.stream().flatMap(song -> Arrays.stream(song.artists)).sorted() - .collect(Collectors.toList()); - } - - @Override - public Collection getGenres() - { - return this.songs.stream().flatMap(song -> Arrays.stream(song.album.genres)).sorted() - .collect(Collectors.toList()); - } - - @Override - public Collection getAlbumArtists() - { - return this.albums.stream().flatMap(album -> Arrays.stream(album.artists)).sorted() - .collect(Collectors.toList()); - } - - @Override - public Collection getYears() - { - return this.albums.stream().map(album -> album.year).sorted() - .collect(Collectors.toList()); - } - - @Override - public Collection getSongsFromAlbum(Album album) - { - return this.songs.stream().filter(song -> song.album == album).sorted() - .collect(Collectors.toList()); - } - - @Override - public Collection 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 getAlbumsFromArtist(String artist) - { - return this.albums.stream().filter(album -> Arrays.asList(album.artists).contains(artist)) - .sorted().collect(Collectors.toList()); - } - - @Override - public Collection getAlbumsFromGenre(String genre) - { - return this.albums.stream().filter(album -> Arrays.asList(album.genres).contains(genre)) - .sorted().collect(Collectors.toList()); - } - - @Override - public Collection 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 - } -} diff --git a/interface/src/main/java/edu/regis/universeplayer/data/SongProvider.java b/interface/src/main/java/edu/regis/universeplayer/data/SongProvider.java index 3353cad..5235304 100644 --- a/interface/src/main/java/edu/regis/universeplayer/data/SongProvider.java +++ b/interface/src/main/java/edu/regis/universeplayer/data/SongProvider.java @@ -6,6 +6,9 @@ package edu.regis.universeplayer.data; import java.io.File; 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. @@ -13,151 +16,84 @@ import java.util.Collection; * @author William Hubbard * @version 0.1 */ -public interface SongProvider +public interface SongProvider extends DataProvider { +// SongProvider 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 INSTANCE = new CompiledSongProvider(new LocalSongProvider(new File(System.getProperty("user.home"), "Music")), InternetSongProvider.getInstance()); - + AlbumProvider getAlbumProvider(); + /** * Obtains all albums within the collection. * * @return A list of albums. */ - Collection getAlbums(); - + default Collection getAlbums() + { + return this.getCollection().stream().map(s -> s.album).collect(Collectors + .toSet()); + } + /** * Obtains all songs within the collection. * * @return A list of songs. */ - Collection getSongs(); - + default Collection getSongs() + { + return this.getCollection(); + } + /** * Obtains a list of all artists. * * @return All artists. */ - Collection getArtists(); - - /** - * Obtains a list of all album artists. - * - * @return All album artists. - */ - Collection getAlbumArtists(); - - /** - * Obtains a list of all genres. - * - * @return All genres. - */ - Collection getGenres(); - - /** - * Obtains a list of all years that have albums. - * - * @return All years. - */ - Collection getYears(); - + default Collection getArtists() + { + return this.getCollection().stream().filter(s -> s.artists != null).map(s -> s.artists) + .mapMulti((BiConsumer>) (strings, objectConsumer) -> { + for (String string : strings) + { + objectConsumer.accept(string); + } + }).collect(Collectors.toSet()); + } + /** * 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. + * @return All songs from the requested album, or null if that album is not + * in the database. */ - Collection getSongsFromAlbum(Album album); - + default Collection getSongsFromAlbum(Album album) + { + return this.getCollection().stream().filter(s -> s.album == album) + .collect(Collectors.toSet()); + } + /** * 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. + * @return A list of all songs from the specified artist, or null if that + * artist is not in the database. */ - Collection 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 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 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 getAlbumsFromYear(int year); - - /** - * Checks to see if we are updating any songs. - * - * @return True if we are updating the song cache. - */ - default boolean isUpdating() + default Collection getSongsFromArtist(String artist) { - 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); } diff --git a/interface/src/main/java/edu/regis/universeplayer/data/UpdateListener.java b/interface/src/main/java/edu/regis/universeplayer/data/UpdateListener.java index a979b01..83a7524 100644 --- a/interface/src/main/java/edu/regis/universeplayer/data/UpdateListener.java +++ b/interface/src/main/java/edu/regis/universeplayer/data/UpdateListener.java @@ -15,10 +15,12 @@ public interface UpdateListener { /** * 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. */ - void onUpdate(int updated, int totalUpdate, String updating); + void onUpdate(DataProvider provider, int updated, int totalUpdate, + String updating); } diff --git a/interface/src/main/java/edu/regis/universeplayer/gui/CollectionList.java b/interface/src/main/java/edu/regis/universeplayer/gui/CollectionList.java index 75b369d..5916c9c 100644 --- a/interface/src/main/java/edu/regis/universeplayer/gui/CollectionList.java +++ b/interface/src/main/java/edu/regis/universeplayer/gui/CollectionList.java @@ -15,7 +15,9 @@ import javax.swing.*; import com.wordpress.tips4java.ScrollablePanel; +import edu.regis.universeplayer.PlayerEnvironment; import edu.regis.universeplayer.data.Album; +import edu.regis.universeplayer.data.AlbumProvider; import edu.regis.universeplayer.data.CollectionType; import edu.regis.universeplayer.data.Song; import edu.regis.universeplayer.data.SongProvider; @@ -144,15 +146,15 @@ public class CollectionList extends ScrollablePanel artistLabel.addActionListener(mouseEvent -> { if (album) { - this.triggerSongDisplayListeners(SongProvider.INSTANCE + this.triggerSongDisplayListeners(PlayerEnvironment.getAlbums() .getAlbumsFromArtist(artist).stream() - .flatMap(album2 -> SongProvider.INSTANCE.getSongsFromAlbum(album2) + .flatMap(album2 -> PlayerEnvironment.getSongs().getSongsFromAlbum(album2) .stream()) .collect(Collectors.toList())); } else { - this.triggerSongDisplayListeners(new ArrayList<>(SongProvider.INSTANCE + this.triggerSongDisplayListeners(new ArrayList<>(PlayerEnvironment.getSongs() .getSongsFromArtist(artist))); } }); @@ -184,7 +186,7 @@ public class CollectionList extends ScrollablePanel albumLabel.setText(album.name); albumLabel.setHorizontalTextPosition(JLabel.CENTER); albumLabel.setVerticalTextPosition(JLabel.BOTTOM); - albumLabel.addActionListener(mouseEvent -> this.triggerSongDisplayListeners(new ArrayList<>(SongProvider.INSTANCE + albumLabel.addActionListener(mouseEvent -> this.triggerSongDisplayListeners(new ArrayList<>(PlayerEnvironment.getSongs() .getSongsFromAlbum(album)))); this.add(albumLabel); this.labelMap.put(albumLabel, album); @@ -222,9 +224,9 @@ public class CollectionList extends ScrollablePanel genreLabel.setText(genre); genreLabel.setHorizontalTextPosition(JLabel.CENTER); genreLabel.setVerticalTextPosition(JLabel.BOTTOM); - genreLabel.addActionListener(mouseEvent -> this.triggerSongDisplayListeners(SongProvider.INSTANCE + genreLabel.addActionListener(mouseEvent -> this.triggerSongDisplayListeners(PlayerEnvironment.getAlbums() .getAlbumsFromGenre(genre).stream() - .flatMap(album2 -> SongProvider.INSTANCE.getSongsFromAlbum(album2) + .flatMap(album2 -> PlayerEnvironment.getSongs().getSongsFromAlbum(album2) .stream()) .collect(Collectors.toList()))); this.add(genreLabel); @@ -263,9 +265,9 @@ public class CollectionList extends ScrollablePanel yearLabel.setText(year.toString()); yearLabel.setHorizontalTextPosition(JLabel.CENTER); yearLabel.setVerticalTextPosition(JLabel.BOTTOM); - yearLabel.addActionListener(mouseEvent -> this.triggerSongDisplayListeners(SongProvider.INSTANCE + yearLabel.addActionListener(mouseEvent -> this.triggerSongDisplayListeners(PlayerEnvironment.getAlbums() .getAlbumsFromYear(year).stream() - .flatMap(album2 -> SongProvider.INSTANCE.getSongsFromAlbum(album2) + .flatMap(album2 -> PlayerEnvironment.getSongs().getSongsFromAlbum(album2) .stream()) .collect(Collectors.toList()))); this.add(yearLabel); diff --git a/interface/src/main/java/edu/regis/universeplayer/gui/Interface.java b/interface/src/main/java/edu/regis/universeplayer/gui/Interface.java index 1cf5f65..39ae70e 100644 --- a/interface/src/main/java/edu/regis/universeplayer/gui/Interface.java +++ b/interface/src/main/java/edu/regis/universeplayer/gui/Interface.java @@ -4,6 +4,7 @@ package edu.regis.universeplayer.gui; +import edu.regis.universeplayer.PlayerEnvironment; import edu.regis.universeplayer.player.Player; import edu.regis.universeplayer.browser.Browser; 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. * - * @param e + * @param e - Event data */ @Override public void windowClosed(WindowEvent e) @@ -234,10 +235,12 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL @Override protected Void doInBackground() throws Exception { - QueryFuture command = PlayerManager.getPlayers().throwError(false); + QueryFuture command = PlayerManager + .getPlayers().throwError(false); try { - if (!command.getConfirmation().wasSuccessful()) + if (!command.getConfirmation() + .wasSuccessful()) { logger.error("Could not run command", command.getConfirmation() @@ -295,7 +298,7 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL if (this.isEnabled()) { collectionTypes - .triggerSongDisplayListeners(new ArrayList<>(SongProvider.INSTANCE + .triggerSongDisplayListeners(new ArrayList<>(PlayerEnvironment.getSongs() .getSongs())); } } @@ -311,7 +314,7 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL if (this.isEnabled()) { collectionTypes - .triggerCollectionDisplayListeners(CollectionType.albumArtist, SongProvider.INSTANCE + .triggerCollectionDisplayListeners(CollectionType.albumArtist, PlayerEnvironment.getAlbums() .getAlbumArtists()); } } @@ -327,7 +330,7 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL if (this.isEnabled()) { collectionTypes - .triggerCollectionDisplayListeners(CollectionType.album, SongProvider.INSTANCE + .triggerCollectionDisplayListeners(CollectionType.album, PlayerEnvironment.getSongs() .getAlbums()); } } @@ -343,7 +346,7 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL if (this.isEnabled()) { collectionTypes - .triggerCollectionDisplayListeners(CollectionType.genre, SongProvider.INSTANCE + .triggerCollectionDisplayListeners(CollectionType.genre, PlayerEnvironment.getAlbums() .getGenres()); } } @@ -359,7 +362,7 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL if (this.isEnabled()) { collectionTypes - .triggerCollectionDisplayListeners(CollectionType.year, SongProvider.INSTANCE + .triggerCollectionDisplayListeners(CollectionType.year, PlayerEnvironment.getAlbums() .getYears()); } } @@ -652,12 +655,14 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL } @Override - public void onUpdate(int updated, int totalUpdate, String updating) + public void onUpdate(DataProvider provider, int updated, + int totalUpdate, String updating) { this.controls.setUpdateProgress(updated, totalUpdate, updating); if (updated == totalUpdate || totalUpdate == 0) { - Collection songs = SongProvider.INSTANCE.getSongs(); + Collection songs = + PlayerEnvironment.getSongs().getSongs(); logger.debug("Resetting the song provider with {} songs.", songs.size()); /* diff --git a/interface/src/main/java/edu/regis/universeplayer/gui/InternetSongDialog.java b/interface/src/main/java/edu/regis/universeplayer/gui/InternetSongDialog.java index 2da5401..5aa00de 100644 --- a/interface/src/main/java/edu/regis/universeplayer/gui/InternetSongDialog.java +++ b/interface/src/main/java/edu/regis/universeplayer/gui/InternetSongDialog.java @@ -145,7 +145,7 @@ public class InternetSongDialog extends JDialog } SwingUtilities.invokeLater(() -> { - Future future = InternetSongProvider.getInstance().addSong(url, this.titleBox.getText(), this.albumBox.getText(), this.artistBox.getText(), this.genreBox.getText()); + Future future = InternetSongProvider.getInstance().addSong(url); InternetSong song = null; try { diff --git a/interface/src/main/java/edu/regis/universeplayer/gui/PlayerControls.java b/interface/src/main/java/edu/regis/universeplayer/gui/PlayerControls.java index 85736d8..3fc05fb 100644 --- a/interface/src/main/java/edu/regis/universeplayer/gui/PlayerControls.java +++ b/interface/src/main/java/edu/regis/universeplayer/gui/PlayerControls.java @@ -271,11 +271,11 @@ public class PlayerControls extends JPanel implements PlaybackListener CommandConfirmation confirmation = status.getConfirmation(); if (status.getConfirmation().wasSuccessful()) { - switch (status.get()) + if (status.get() == PlaybackStatus.PLAYING) { - case PLAYING -> confirmation = - PlayerManager.getPlayers().pause() - .getConfirmation(); + confirmation = + PlayerManager.getPlayers().pause() + .getConfirmation(); } } if (confirmation != null && !confirmation.wasSuccessful()) @@ -379,7 +379,7 @@ public class PlayerControls extends JPanel implements PlaybackListener void setUpdateProgress(int updated, int toUpdate, String updating) { this.updateProgress.setString(updating); - if (toUpdate == 0) + if (toUpdate == 0 || updated == toUpdate) { this.updateProgress.setVisible(false); // 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.repaint(); } /** diff --git a/interface/src/main/java/edu/regis/universeplayer/gui/SongList.java b/interface/src/main/java/edu/regis/universeplayer/gui/SongList.java index c012225..6131560 100644 --- a/interface/src/main/java/edu/regis/universeplayer/gui/SongList.java +++ b/interface/src/main/java/edu/regis/universeplayer/gui/SongList.java @@ -10,6 +10,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import edu.regis.universeplayer.ClickListener; +import edu.regis.universeplayer.PlayerEnvironment; import edu.regis.universeplayer.data.Queue; import edu.regis.universeplayer.data.*; @@ -44,7 +45,7 @@ public class SongList extends ScrollablePanel this.setFocusTraversalPolicyProvider(true); this.setLayout(layout); - SongProvider provider = SongProvider.INSTANCE; + SongProvider provider = PlayerEnvironment.getSongs(); this.listAlbums(provider.getSongs()); this.setScrollableWidth(ScrollableSizeHint.FIT); @@ -98,230 +99,250 @@ public class SongList extends ScrollablePanel * thread. * * @return the computed result - * @throws Exception if unable to compute a result */ @Override - protected Object doInBackground() throws Exception + protected Object doInBackground() { - logger.debug("Sorting {} songs...", songs.size()); - Map> albums = songs.stream().sorted().collect(Collectors - .groupingBy(song -> song.album, Collectors - .mapping(song -> (Song) song, Collectors.toList()))); - logger.debug("Listing {} albums ({} songs)", - albums.size(), songs.size()); - GridBagConstraints c = new GridBagConstraints(); - c.fill = GridBagConstraints.HORIZONTAL; - AtomicInteger i = new AtomicInteger(0); + try + { + logger.debug("Sorting {} songs...", songs.size()); + Map> albums = + songs.stream().filter(s -> s.album != null).sorted() + .collect(Collectors + .groupingBy(song -> song.album, Collectors + .mapping(song -> (Song) song, Collectors + .toList()))); + logger.debug("Listing {} albums ({} songs)", + albums.size(), songs.size()); + GridBagConstraints c = new GridBagConstraints(); + c.fill = GridBagConstraints.HORIZONTAL; + AtomicInteger i = new AtomicInteger(0); - SwingUtilities.invokeLater(() -> { - labelMap.clear(); - artMap.clear(); - removeAll(); - currentAlbums = albums; - }); + SwingUtilities.invokeLater(() -> { + labelMap.clear(); + artMap.clear(); + removeAll(); + currentAlbums = albums; + }); - LinkedHashMap albumInfos = - new LinkedHashMap<>(); - albums.keySet().stream().sorted().forEach((album) -> { - List songCollection = albums.get(album); + LinkedHashMap albumInfos = + new LinkedHashMap<>(); + albums.keySet().stream().sorted().forEach((album) -> { + List songCollection = albums.get(album); - AlbumInfo albumInfo = new AlbumInfo(album); - c.gridx = 0; - c.gridy = i.get(); - c.gridwidth = 1; - c.gridheight = songCollection.size(); - c.weightx = 0; - c.anchor = GridBagConstraints.NORTHWEST; - c.insets = new Insets(0, 0, 20, 10); - List finalSongCollection = songCollection; - albumInfo.addMouseListener((ClickListener) e -> { - if (e.getClickCount() == 2) - { - Queue.getInstance().addAll(finalSongCollection); - } - }); - albumInfo.albumName.addMouseListener((ClickListener) e -> { - if (e.getClickCount() == 2) - { - Container inter = SongList.this; - do - { - inter = inter.getParent(); - } - while (!(inter instanceof Interface) && inter - .getParent() != null); - if (inter instanceof Interface) - { - ((Interface) inter).updateSongs(SongProvider.INSTANCE - .getSongsFromAlbum(albumInfo.album)); - } - } - }); - albumInfo.artists.addMouseListener((ClickListener) e -> { - if (e.getClickCount() == 2) - { - Container inter = SongList.this; - do - { - inter = inter.getParent(); - } - while (!(inter instanceof Interface) && inter - .getParent() != null); - if (inter instanceof Interface) - { - ((Interface) inter) - .updateCollections(CollectionType.album, Arrays - .stream(albumInfo.album.artists) - .flatMap(s -> SongProvider.INSTANCE - .getAlbumsFromArtist(s) - .stream()) - .collect(Collectors.toList())); - } - } - }); - albumInfo.genres.addMouseListener((ClickListener) e -> { - if (e.getClickCount() == 2) - { - Container inter = SongList.this; - do - { - inter = inter.getParent(); - } - while (!(inter instanceof Interface) && inter - .getParent() != null); - if (inter instanceof Interface) - { - ((Interface) inter) - .updateCollections(CollectionType.album, Arrays - .stream(albumInfo.album.genres) - .flatMap(s -> SongProvider.INSTANCE - .getAlbumsFromGenre(s).stream()) - .collect(Collectors.toList())); - } - } - }); - albumInfo.year.addMouseListener((ClickListener) e -> { - if (e.getClickCount() == 2) - { - Container inter = SongList.this; - do - { - inter = inter.getParent(); - } - while (!(inter instanceof Interface) && inter - .getParent() != null); - if (inter instanceof Interface) - { - ((Interface) inter) - .updateCollections(CollectionType.album, SongProvider.INSTANCE - .getAlbumsFromYear(albumInfo.album.year)); - } - } - }); - albumInfos.put(albumInfo, (GridBagConstraints) c.clone()); - artMap.put(albumInfo, album); - - JButton firstSong = null; - - JLabel songNum; - JButton songTitle; - AtomicInteger numSongs = new AtomicInteger(); - for (Song song : songCollection) - { - songNum = new JLabel(String.valueOf(song.trackNum)); - songNum.setFocusable(false); - c.gridx = 1; + AlbumInfo albumInfo = new AlbumInfo(album); + c.gridx = 0; c.gridy = i.get(); - c.gridheight = 1; + c.gridwidth = 1; + c.gridheight = songCollection.size(); c.weightx = 0; - c.anchor = GridBagConstraints.NORTHEAST; - c.insets = new Insets(0, 0, 0, 0); - albumInfos.put(songNum, (GridBagConstraints) c.clone()); - labelMap.put(songNum, song); - - songTitle = new JButton(song.title); - if (song.title == null || song.title.isEmpty()) - { - if (song instanceof LocalSong) + c.anchor = GridBagConstraints.NORTHWEST; + c.insets = new Insets(0, 0, 20, 10); + albumInfo.addMouseListener((ClickListener) e -> { + if (e.getClickCount() == 2) { - songTitle.setText(((LocalSong) song).file.getName()); - } - } - songTitle.setHorizontalAlignment(JButton.LEFT); - songTitle.setFocusPainted(true); - songTitle.setMargin(new Insets(0, 0, 0, 0)); - songTitle.setContentAreaFilled(false); - songTitle.setBorderPainted(false); - songTitle.setOpaque(false); - songTitle.addActionListener(new AbstractAction() - { - @Override - public void actionPerformed(ActionEvent e) - { - Queue.getInstance().add(song); - Queue.getInstance() - .skipToSong(Queue.getInstance().size() - 1); + Queue.getInstance().addAll(songCollection); } }); - c.gridx = 2; - c.gridy = i.get(); - c.weightx = 1.0; - c.anchor = GridBagConstraints.NORTHWEST; - c.insets = new Insets(0, 10, 0, 0); - albumInfos.put(songTitle, (GridBagConstraints) c.clone()); - labelMap.put(songTitle, song); - // TODO - Add song length or something + albumInfo.albumName + .addMouseListener((ClickListener) e -> { + if (e.getClickCount() == 2) + { + Container inter = SongList.this; + do + { + inter = inter.getParent(); + } + while (!(inter instanceof Interface) && inter + .getParent() != null); + if (inter instanceof Interface) + { + ((Interface) inter) + .updateSongs(PlayerEnvironment.getSongs() + .getSongsFromAlbum(albumInfo.album)); + } + } + }); + albumInfo.artists + .addMouseListener((ClickListener) e -> { + if (e.getClickCount() == 2) + { + Container inter = SongList.this; + do + { + inter = inter.getParent(); + } + while (!(inter instanceof Interface) && inter + .getParent() != null); + if (inter instanceof Interface) + { + ((Interface) inter) + .updateCollections(CollectionType.album, Arrays + .stream(albumInfo.album.artists) + .flatMap(s -> PlayerEnvironment.getAlbums() + .getAlbumsFromArtist(s) + .stream()) + .collect(Collectors + .toList())); + } + } + }); + albumInfo.genres.addMouseListener((ClickListener) e -> { + if (e.getClickCount() == 2) + { + Container inter = SongList.this; + do + { + inter = inter.getParent(); + } + while (!(inter instanceof Interface) && inter + .getParent() != null); + if (inter instanceof Interface) + { + ((Interface) inter) + .updateCollections(CollectionType.album, Arrays + .stream(albumInfo.album.genres) + .flatMap(s -> PlayerEnvironment.getAlbums() + .getAlbumsFromGenre(s) + .stream()) + .collect(Collectors + .toList())); + } + } + }); + albumInfo.year.addMouseListener((ClickListener) e -> { + if (e.getClickCount() == 2) + { + Container inter = SongList.this; + do + { + inter = inter.getParent(); + } + while (!(inter instanceof Interface) && inter + .getParent() != null); + if (inter instanceof Interface) + { + ((Interface) inter) + .updateCollections(CollectionType.album, PlayerEnvironment.getAlbums() + .getAlbumsFromYear(albumInfo.album.year)); + } + } + }); + albumInfos + .put(albumInfo, (GridBagConstraints) c.clone()); + artMap.put(albumInfo, album); - if (firstSong == null) + JButton firstSong = null; + + JLabel songNum; + JButton songTitle; + AtomicInteger numSongs = new AtomicInteger(); + for (Song song : songCollection) { - firstSong = songTitle; - JButton finalFirstSong = firstSong; - albumInfo.setAction(new AbstractAction() + songNum = new JLabel(String.valueOf(song.trackNum)); + songNum.setFocusable(false); + c.gridx = 1; + c.gridy = i.get(); + c.gridheight = 1; + c.weightx = 0; + c.anchor = GridBagConstraints.NORTHEAST; + c.insets = new Insets(0, 0, 0, 0); + albumInfos.put(songNum, (GridBagConstraints) c + .clone()); + labelMap.put(songNum, song); + + songTitle = new JButton(song.title); + if (song.title == null || song.title.isEmpty()) + { + if (song instanceof LocalSong) + { + songTitle.setText(((LocalSong) song).file + .getName()); + } + } + songTitle.setHorizontalAlignment(JButton.LEFT); + songTitle.setFocusPainted(true); + songTitle.setMargin(new Insets(0, 0, 0, 0)); + songTitle.setContentAreaFilled(false); + songTitle.setBorderPainted(false); + songTitle.setOpaque(false); + songTitle.addActionListener(new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { - finalFirstSong.requestFocusInWindow(); + Queue.getInstance().add(song); + Queue.getInstance() + .skipToSong(Queue.getInstance() + .size() - 1); } }); - List finalSongCollection1 = songCollection; - albumInfo.addKeyListener(new KeyAdapter() + c.gridx = 2; + c.gridy = i.get(); + c.weightx = 1.0; + c.anchor = GridBagConstraints.NORTHWEST; + c.insets = new Insets(0, 10, 0, 0); + albumInfos.put(songTitle, (GridBagConstraints) c + .clone()); + labelMap.put(songTitle, song); + // TODO - Add song length or something + + if (firstSong == null) { - @Override - public void keyTyped(KeyEvent e) + firstSong = songTitle; + JButton finalFirstSong = firstSong; + albumInfo.setAction(new AbstractAction() { - if (e.getKeyCode() == KeyEvent.VK_ENTER) + @Override + public void actionPerformed(ActionEvent e) { - Queue.getInstance() - .addAll(finalSongCollection1); + finalFirstSong.requestFocusInWindow(); } - } - }); + }); + albumInfo.addKeyListener(new KeyAdapter() + { + @Override + public void keyTyped(KeyEvent e) + { + if (e.getKeyCode() == KeyEvent.VK_ENTER) + { + Queue.getInstance() + .addAll(songCollection); + } + } + }); + } + i.getAndIncrement(); + this.setProgress((int) (numSongs + .incrementAndGet() / (float) songs + .size() * 100F)); } - i.getAndIncrement(); - this.setProgress((int) (numSongs.incrementAndGet() / (float) songs.size() * 100F)); - } // this.add(new JLabel(new ImageIcon(this.getClass().getResource("/gui/icons/defaultart.png"), "Default")), c); - c.gridx = 0; - c.gridy = i.getAndIncrement(); - c.gridwidth = 3; - c.anchor = GridBagConstraints.NORTH; - albumInfos.put(new JSeparator(SwingConstants.HORIZONTAL), - (GridBagConstraints) c.clone()); + c.gridx = 0; + c.gridy = i.getAndIncrement(); + c.gridwidth = 3; + c.anchor = GridBagConstraints.NORTH; + albumInfos + .put(new JSeparator(SwingConstants.HORIZONTAL), + (GridBagConstraints) c.clone()); - i.getAndIncrement(); - }); - logger.debug("Song list built {} components", - albumInfos.size()); - SwingUtilities.invokeLater(() -> { - albumInfos.forEach((component, c1) -> { - add(component, c1); + i.getAndIncrement(); }); - revalidate(); - logger.debug("Components added"); - }); - return null; + logger.debug("Song list built {} components", + albumInfos.size()); + SwingUtilities.invokeLater(() -> { + albumInfos.forEach((component, c1) -> add(component, c1)); + revalidate(); + logger.debug("Components added"); + }); + return null; + } + catch (Exception e) + { + logger.error("Could not list songs", e); + return null; + } } }; worker.execute(); diff --git a/interface/src/main/java/edu/regis/universeplayer/player/BrowserPlayer.java b/interface/src/main/java/edu/regis/universeplayer/player/BrowserPlayer.java index 609ccc1..b0fdd6c 100644 --- a/interface/src/main/java/edu/regis/universeplayer/player/BrowserPlayer.java +++ b/interface/src/main/java/edu/regis/universeplayer/player/BrowserPlayer.java @@ -110,9 +110,8 @@ public class BrowserPlayer implements Player, UpdateListener { try { - QueryFuture future = new ForwardedFuture(getBrowser() + return (QueryFuture) new ForwardedFuture(getBrowser() .sendObject(new CommandQuit())); - return future; } catch (IOException e) { @@ -190,10 +189,21 @@ public class BrowserPlayer implements Player, UpdateListener { try { - return new ForwardedFuture(getBrowser() - .sendObject(new CommandSetPlayback(CommandSetPlayback.Playback.PAUSE))); + QueryFuture future = this.getStatus(); + switch (future.get()) + { + case PLAYING -> { + return new ForwardedFuture(getBrowser() + .sendObject(new CommandSetPlayback(CommandSetPlayback.Playback.PAUSE))); + } + case PAUSED, STOPPED, FINISHED -> { + return new ForwardedFuture(getBrowser() + .sendObject(new CommandSetPlayback(CommandSetPlayback.Playback.PLAY))); + } + } + return null; } - catch (IOException e) + catch (IOException | InterruptedException | ExecutionException e) { logger.error("Could not send message", e); return null; @@ -326,7 +336,7 @@ public class BrowserPlayer implements Player, UpdateListener * * @param forward - Whether the error should be thrown from the foreground * script or the background script. - * @return + * @return The return value containing error details. */ public QueryFuture throwError(boolean forward) { @@ -349,6 +359,7 @@ public class BrowserPlayer implements Player, UpdateListener if (object instanceof PlaybackInfo) { status = new PlaybackEvent(this, (PlaybackInfo) object); + logger.info("Internet playback {}", status.getInfo()); this.listeners.forEach(l -> l.onPlaybackChanged(status)); } } diff --git a/interface/src/main/resources/lang/interface.properties b/interface/src/main/resources/lang/interface.properties index 8fa82b8..df28a13 100644 --- a/interface/src/main/resources/lang/interface.properties +++ b/interface/src/main/resources/lang/interface.properties @@ -21,6 +21,8 @@ albumInfo.artists=Artists albumInfo.genres=Genres albumInfo.year=Year +update.database=Querying Database: %s +update.local=Querying File System: %s interface.queue.title=Queue