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 036db3c..562b8bf 100644 --- a/browserCommands/src/main/java/edu/regis/universeplayer/data/Album.java +++ b/browserCommands/src/main/java/edu/regis/universeplayer/data/Album.java @@ -4,6 +4,8 @@ package edu.regis.universeplayer.data; +import java.util.Arrays; + import javax.swing.ImageIcon; public class Album implements Comparable @@ -20,13 +22,22 @@ public class Album implements Comparable @Override public int compareTo(Album o) { - if (o != null) + if (o != null && o.name != null) { - return this.name.compareTo(o.name); + return this.name.compareToIgnoreCase(o.name); } else { return -1; } } + + @Override + public String toString() + { + return "Album{" + + "name='" + name + '\'' + + ", artists=" + Arrays.toString(artists) + + '}'; + } } 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 91e8903..f60f152 100644 --- a/browserCommands/src/main/java/edu/regis/universeplayer/data/InternetSong.java +++ b/browserCommands/src/main/java/edu/regis/universeplayer/data/InternetSong.java @@ -4,9 +4,52 @@ package edu.regis.universeplayer.data; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URISyntaxException; import java.net.URL; +import java.util.Arrays; public class InternetSong extends Song { + private static final Logger logger = LoggerFactory + .getLogger(InternetSong.class); public URL location; + + @Override + public int compareTo(Song o) + { + int compare = super.compareTo(o); + if (compare == 0) + { + if (o instanceof LocalSong) + { + try + { + compare = + this.location.toURI() + .compareTo(((InternetSong) o).location + .toURI()); + } + catch (URISyntaxException e) + { + logger.error("Could not compare locations {} and {}", + this.location, ((InternetSong) o).location, e); + } + } + } + return compare; + } + + @Override + public String toString() + { + return "Song{" + + "title='" + title + '\'' + + ", artists=" + Arrays.toString(artists) + + ", album=" + album + + ", url=" + location + + '}'; + } } 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 74d1483..73ecb0e 100644 --- a/browserCommands/src/main/java/edu/regis/universeplayer/data/LocalSong.java +++ b/browserCommands/src/main/java/edu/regis/universeplayer/data/LocalSong.java @@ -5,6 +5,7 @@ package edu.regis.universeplayer.data; import java.io.File; +import java.util.Arrays; /** * This song represents a song found on the local file system. @@ -14,4 +15,29 @@ public class LocalSong extends Song public File file; public String type; public String codec; + + @Override + public int compareTo(Song o) + { + int compare = super.compareTo(o); + if (compare == 0) + { + if (o instanceof LocalSong) + { + compare = this.file.compareTo(((LocalSong) o).file); + } + } + return compare; + } + + @Override + public String toString() + { + return "Song{" + + "title='" + title + '\'' + + ", artists=" + Arrays.toString(artists) + + ", album=" + album + + ", url=" + file.getAbsolutePath() + + '}'; + } } 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 68a3490..42c4bee 100644 --- a/browserCommands/src/main/java/edu/regis/universeplayer/data/Song.java +++ b/browserCommands/src/main/java/edu/regis/universeplayer/data/Song.java @@ -5,6 +5,7 @@ package edu.regis.universeplayer.data; import java.io.Serializable; +import java.util.Arrays; /** * Contains data for a song. @@ -16,18 +17,20 @@ public class Song implements Comparable, Serializable public int trackNum; public int disc; public long duration; - + /** * A reference to the album this song is part of. */ public Album album; - + @Override public int compareTo(Song o) { if (o != null) { - int comp = this.album != null ? this.album.compareTo(o.album) : o.album != null ? 1 : 0; + int comp = this.album != null ? + (o.album != null ? this.album.compareTo(o.album) : + -1) : o.album != null ? 1 : 0; if (comp == 0) { comp = Integer.compare(this.disc, o.disc); @@ -36,7 +39,10 @@ public class Song implements Comparable, Serializable comp = Integer.compare(this.trackNum, o.trackNum); if (comp == 0) { - comp = this.title != null ? this.title.compareTo(o.title) : o.title != null ? 1 : 0; + comp = this.title != null ? + (o.title != null ? + this.title.compareTo(o.title) : + -1) : o.title != null ? 1 : 0; } } } @@ -47,4 +53,14 @@ public class Song implements Comparable, Serializable return -1; } } + + @Override + public String toString() + { + return "Song{" + + "title='" + title + '\'' + + ", artists=" + Arrays.toString(artists) + + ", album=" + album + + '}'; + } } 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 729da46..e29ff48 100644 --- a/interface/src/main/java/edu/regis/universeplayer/data/LocalSongProvider.java +++ b/interface/src/main/java/edu/regis/universeplayer/data/LocalSongProvider.java @@ -5,6 +5,7 @@ package edu.regis.universeplayer.data; import edu.regis.universeplayer.player.Interface; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -19,12 +20,16 @@ import java.util.stream.Collectors; public class LocalSongProvider implements SongProvider { - private static final Logger logger = LoggerFactory.getLogger(LocalSongProvider.class); + private static final Logger logger = LoggerFactory + .getLogger(LocalSongProvider.class); 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 File source; - + private final HashMap songs = new HashMap<>(); private final HashMap albums = new HashMap<>(); /** @@ -43,11 +48,11 @@ public class LocalSongProvider implements SongProvider * 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<>(); - + /** * Obtains all formats supported by FFMPEG. Note that this list includes * video and image formats as well. @@ -56,22 +61,26 @@ 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 \\(\\)-/'\\.\":]+$"); - final Pattern FILEPAT2 = Pattern.compile("[a-z1-9_]{2,}(,[a-z1-9_]{2,})*"); + final Pattern FILEPAT = Pattern + .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; String ffmpegData; String name; - + synchronized (formats) { if (formats.isEmpty()) { try { - Process process = Runtime.getRuntime().exec(new String[] {"ffmpeg", "-formats"}); + Process process = Runtime.getRuntime() + .exec(new String[]{"ffmpeg", "-formats"}); logger.debug("Getting formats"); logger.debug("Process complete"); - try (Scanner scanner = new Scanner(process.getInputStream())) + try (Scanner scanner = new Scanner(process + .getInputStream())) { while (scanner.hasNextLine()) { @@ -99,7 +108,7 @@ public class LocalSongProvider implements SongProvider } return formats; } - + /** * Obtains all audio formats supported by FFMPEG. * @@ -107,12 +116,13 @@ public class LocalSongProvider implements SongProvider */ public static Set getCodecs() { - final Pattern FILEPAT = Pattern.compile("^\\s*D[E.]A[I.][L.][S.]\\s*([a-z1-9_]{2,})\\s*.+$"); + final Pattern FILEPAT = Pattern + .compile("^\\s*D[E.]A[I.][L.][S.]\\s*([a-z1-9_]{2,})\\s*.+$"); final Pattern FILEPAT2 = Pattern.compile("[a-z1-9_]{2,}"); Matcher matcher; String ffmpegData; String name; - + synchronized (codecs) { if (codecs.isEmpty()) @@ -120,9 +130,11 @@ public class LocalSongProvider implements SongProvider try { logger.debug("Getting codecs"); - Process process = Runtime.getRuntime().exec(new String[] {"ffmpeg", "-codecs"}); + Process process = Runtime.getRuntime() + .exec(new String[]{"ffmpeg", "-codecs"}); logger.debug("Process complete"); - try (Scanner scanner = new Scanner(process.getInputStream())) + try (Scanner scanner = new Scanner(process + .getInputStream())) { while (scanner.hasNextLine()) { @@ -150,7 +162,7 @@ public class LocalSongProvider implements SongProvider } return codecs; } - + public LocalSongProvider(File source) { Connection dbL = null; @@ -161,12 +173,12 @@ public class LocalSongProvider implements SongProvider } getSongCache(); } - + private void getSongCache() { - SongScanner.service.submit(new SongQuery(true)); + service.submit(new SongQuery(true)); } - + /** * Obtains all albums within the collection. * @@ -177,7 +189,7 @@ public class LocalSongProvider implements SongProvider { return this.albums.values(); } - + /** * Obtains all songs within the collection. * @@ -191,7 +203,7 @@ public class LocalSongProvider implements SongProvider return Collections.unmodifiableCollection(this.songs.values()); } } - + /** * Obtains a list of all artists. * @@ -202,7 +214,7 @@ public class LocalSongProvider implements SongProvider { return this.artists; } - + /** * Obtains a list of all album artists. * @@ -213,7 +225,7 @@ public class LocalSongProvider implements SongProvider { return this.albumArtists; } - + /** * Obtains a list of all genres. * @@ -224,7 +236,7 @@ public class LocalSongProvider implements SongProvider { return this.genres; } - + /** * Obtains a list of all years that have albums. * @@ -235,44 +247,50 @@ public class LocalSongProvider implements SongProvider { 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. + * @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.songs) { - return this.songs.values().stream().filter(song -> song.album.equals(album)).collect(Collectors.toUnmodifiableSet()); + 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. + * @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.songs) { - return this.songs.values().stream().filter(song -> Arrays.asList(song.artists).contains(artist)).collect(Collectors.toUnmodifiableSet()); + 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. + * @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) @@ -282,7 +300,7 @@ public class LocalSongProvider implements SongProvider return this.albums.get(name); } } - + /** * Obtains all albums that were written by a certain artist. * @@ -294,10 +312,13 @@ public class LocalSongProvider implements SongProvider { synchronized (this.albums) { - return this.albums.values().stream().filter(album -> Arrays.asList(album.artists).contains(artist)).collect(Collectors.toUnmodifiableSet()); + return this.albums.values().stream() + .filter(album -> Arrays.asList(album.artists) + .contains(artist)) + .collect(Collectors.toUnmodifiableSet()); } } - + /** * Obtains all albums that match a certain genre * @@ -309,10 +330,13 @@ public class LocalSongProvider implements SongProvider { synchronized (this.albums) { - return this.albums.values().stream().filter(album -> Arrays.asList(album.genres).contains(genre)).collect(Collectors.toUnmodifiableSet()); + 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. * @@ -324,67 +348,68 @@ public class LocalSongProvider implements SongProvider { synchronized (this.albums) { - return this.albums.values().stream().filter(album -> album.year == year).collect(Collectors.toUnmodifiableSet()); + return this.albums.values().stream() + .filter(album -> album.year == year) + .collect(Collectors.toUnmodifiableSet()); } } - + @Override public int getUpdateProgress() { return this.updatedSongs; } - + @Override public int getTotalUpdateSongs() { return this.totalUpdate; } - + @Override public String getUpdateText() { - return SongScanner.currentFolder; + return currentFolder; } - + @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())); + this.listeners.forEach(listener -> listener + .onUpdate(this.getUpdateProgress(), this + .getTotalUpdateSongs(), this.getUpdateText())); } - + private class SongScanner extends RecursiveAction { - private static final ForkJoinPool service = new ForkJoinPool(); - private static String currentFolder; - private final File file; - + SongScanner(File folder) { this.file = folder; } - + @Override public void compute() { Process process; String line; String[] streamData; - + String type; String codec; - + String genre = null; String title = null; String artist = null; @@ -393,26 +418,34 @@ public class LocalSongProvider implements SongProvider long duration = 0; Integer[] track = null; Integer[] disc = null; - + Statement state = null; ResultSet result; - + try { - if (file.getName().lastIndexOf(".") < file.getName().length() - 1) + if (file.getName().lastIndexOf(".") < file.getName() + .length() - 1) { - type = file.getName().substring(file.getName().lastIndexOf('.') + 1).toLowerCase(); + type = file.getName() + .substring(file.getName().lastIndexOf('.') + 1) + .toLowerCase(); if (getFormats().contains(type)) { try { synchronized (DatabaseManager.getDb()) { - state = DatabaseManager.getDb().createStatement(); - result = state.executeQuery("SELECT mod FROM local_songs WHERE file='" + this.file.getAbsolutePath().replaceAll("'", "''") + "';"); + 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()) + if (result.getLong(1) >= this.file + .lastModified()) { /* * No modifications needed @@ -424,148 +457,202 @@ public class LocalSongProvider implements SongProvider } else { - state.executeUpdate("UPDATE local_songs SET mod = " + this.file.lastModified() + " WHERE file='" + this.file.getAbsolutePath().replaceAll("'", "''") + "';"); + state.executeUpdate("UPDATE local_songs SET mod = " + this.file + .lastModified() + " WHERE file='" + this.file + .getAbsolutePath() + .replaceAll("'", "''") + "';"); } } } currentFolder = file.getPath(); codec = null; - process = Runtime.getRuntime().exec(new String[] {"ffprobe", "-hide_banner", file.getAbsolutePath()}); + process = Runtime.getRuntime() + .exec(new String[]{"ffprobe", "-hide_banner", file.getAbsolutePath()}); process.waitFor(); - try (Scanner scanner = new Scanner(process.getErrorStream())) + try (Scanner scanner = new Scanner(process + .getErrorStream())) { int i = 0; while (scanner.hasNextLine()) { line = scanner.nextLine().trim(); - switch (line.toLowerCase().substring(0, line.indexOf(' ') > 0 ? line.indexOf(' ') : line.length())) + try { - case "genre" -> { - /* - * We only take the first one, as to - * avoid mishaps with labels after the - * metadata. - */ - if (genre == null) + switch (line.toLowerCase() + .substring(0, line + .indexOf(' ') > 0 ? line + .indexOf(' ') : line + .length())) { - genre = line.substring(line.indexOf(':') + 2); + 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 "title" -> { + if (title == null) + { + title = line.substring(line + .indexOf(':') + 2); + } } - } - case "artist" -> { - if (artist == null) - { - artist = 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" -> { + if (albumTitle == null) + { + albumTitle = line.substring(line + .indexOf(':') + 2); + } } - } - case "album_artist" -> { - if (albumArtist == null) - { - albumArtist = line.substring(line.indexOf(':') + 2); + case "album_artist" -> { + if (albumArtist == null) + { + albumArtist = line + .substring(line + .indexOf(':') + 2); + } } - } - case "track" -> { - line = line.substring(line.indexOf(':') + 2); - if (line.indexOf('/') >= 0) - { - track = Arrays.stream(line.split("/")).map(Integer::parseInt).toArray(Integer[]::new); + case "track" -> { + line = line.substring(line + .indexOf(':') + 2); + if (line.indexOf('/') >= 0) + { + track = Arrays + .stream(line.split("/")) + .map(Integer::parseInt) + .toArray(Integer[]::new); + } + else + { + if (track != null) + { + track[0] = Integer + .parseInt(line); + } + else + { + track = new Integer[]{Integer.parseInt(line), -1}; + } + } } - else - { + case "tracktotal" -> { if (track != null) { - track[0] = Integer.parseInt(line); + track[1] = Integer.parseInt(line + .substring(line + .indexOf(':') + 2)); } else { - track = new Integer[] {Integer.parseInt(line), -1}; + track = new Integer[]{-1, Integer.parseInt(line + .substring(line + .indexOf(':') + 2))}; } } - } - case "tracktotal" -> { - if (track != null) - { - track[1] = Integer.parseInt(line.substring(line.indexOf(':') + 2)); + case "disc" -> { + line = line.substring(line + .indexOf(':') + 2); + if (line.indexOf('/') >= 0) + { + disc = Arrays + .stream(line.split("/")) + .map(Integer::parseInt) + .toArray(Integer[]::new); + } + else + { + if (disc != null) + { + disc[0] = Integer + .parseInt(line); + } + else + { + disc = new Integer[]{Integer.parseInt(line), -1}; + } + } } - else - { - track = new Integer[] {-1, Integer.parseInt(line.substring(line.indexOf(':') + 2))}; - } - } - case "disc" -> { - line = line.substring(line.indexOf(':') + 2); - if (line.indexOf('/') >= 0) - { - disc = Arrays.stream(line.split("/")).map(Integer::parseInt).toArray(Integer[]::new); - } - else - { + case "disctotal" -> { if (disc != null) { - disc[0] = Integer.parseInt(line); + disc[1] = Integer.parseInt(line + .substring(line + .indexOf(':') + 2)); } else { - disc = new Integer[] {Integer.parseInt(line), -1}; + disc = new Integer[]{-1, Integer.parseInt(line + .substring(line + .indexOf(':') + 2))}; } } - } - 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) - { - line = line.substring(line.indexOf(':') + 2, line.indexOf(',')); - duration = Long.parseLong(line.substring(0, 2)) * 3600 * 1000 + Long.parseLong(line.substring(3, 5)) * 60 * 1000 + Long.parseLong(line.substring(6, 8)) * 1000 + (long) (Float.parseFloat(line.substring(8, line.length() - 1)) * 1000); - } - } - case "stream" -> { - streamData = line.split(" "); - if (streamData[2].equals("Audio:")) - { - codec = streamData[3]; - if (codec.endsWith(",")) + case "duration:" -> { + if (duration == 0) { - codec = codec.substring(0, codec.length() - 1); + line = line.substring(line + .indexOf(':') + 2, line + .indexOf(',')); + duration = Long.parseLong(line + .substring(0, 2)) * 3600 * 1000 + Long + .parseLong(line + .substring(3, 5)) * 60 * 1000 + Long + .parseLong(line + .substring(6, 8)) * 1000 + (long) (Float + .parseFloat(line + .substring(8, line + .length() - 1)) * 1000); } - /* - * If this isn't a supported codec, - * discard. - */ - if (!getCodecs().contains(codec)) + } + case "stream" -> { + streamData = line.split(" "); + if (streamData[2].equals("Audio:")) { - logger.trace("Invalid codec {} for song {}", codec, file); - codec = null; + 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 codec {} for song {}", codec, file); + logger.trace("Found non-audio stream {} for {}", line, 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); @@ -580,54 +667,98 @@ public class LocalSongProvider implements SongProvider /* * This part in particular is prone to thread-safety issues. */ - result = state.executeQuery("SELECT album FROM local_albums WHERE album='" + albumTitle + "';"); + 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) + result = state + .executeQuery("SELECT artists FROM local_albums WHERE album='" + albumTitle + "';"); + if (result + .getString("artists") == null && albumArtist != null) { - state.executeUpdate("UPDATE local_albums SET artists='" + Arrays.stream(albumArtist.split(";")).map(String::trim).collect(Collectors.joining(";")) + "' WHERE album='" + albumTitle + "';"); + 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) + result = state + .executeQuery("SELECT genres FROM local_albums WHERE album='" + albumTitle + "';"); + if (result + .getString("genres") == null && genre != null) { - state.executeUpdate("UPDATE local_albums SET genres='" + Arrays.stream(genre.split(";")).map(String::trim).collect(Collectors.joining(";")) + "' WHERE album='" + albumTitle + "';"); + 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) + 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) + 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("'", "''") + "';"); + 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("', "); + sql.append("codec='").append(codec) + .append("', "); + sql.append("type='").append(codec) + .append("', "); if (title != null && !title.isEmpty()) { - sql.append("title='").append(title.replaceAll("'", "''")).append("', "); + sql.append("title='").append(title + .replaceAll("'", "''")) + .append("', "); } else { - sql.append("title='").append(file.getName().replaceAll("'", "''")).append("', "); + 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).collect(Collectors.joining(";"))).append("', "); + sql.append("artists='") + .append(Optional.of(artist) + .map(s -> s + .split(";")) + .stream() + .flatMap(Arrays::stream) + .map(String::trim) + .collect(Collectors + .joining(";"))) + .append("', "); } else { @@ -635,7 +766,8 @@ public class LocalSongProvider implements SongProvider } if (track != null && track[0] > 0) { - sql.append("track=").append(track[0]).append(", "); + sql.append("track=") + .append(track[0]).append(", "); } else { @@ -643,7 +775,8 @@ public class LocalSongProvider implements SongProvider } if (disc != null && disc[0] > 0) { - sql.append("disc=").append(disc[0]).append(", "); + sql.append("disc=").append(disc[0]) + .append(", "); } else { @@ -651,22 +784,30 @@ public class LocalSongProvider implements SongProvider } if (duration != 0) { - sql.append("duration=").append(duration).append(", "); + sql.append("duration=") + .append(duration).append(", "); } else { sql.append("duration=NULL, "); } - if (albumTitle != null && !albumTitle.isEmpty()) + if (albumTitle != null && !albumTitle + .isEmpty()) { - sql.append("album='").append(albumTitle).append("', "); + 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("';"); + sql.append("mod=") + .append(file.lastModified()); + sql.append(" WHERE file='") + .append(file.getAbsolutePath() + .replaceAll("'", "''")) + .append("';"); state.executeUpdate(sql.toString()); } else @@ -675,22 +816,38 @@ public class LocalSongProvider implements SongProvider 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("',"); + values.append('\'') + .append(file.getAbsolutePath() + .replaceAll("'", "''")) + .append("',"); columns.append("codec,"); - values.append('\'').append(codec).append("',"); + values.append('\'').append(codec) + .append("',"); columns.append("type,"); - values.append('\'').append(type).append("',"); + values.append('\'').append(type) + .append("',"); if (title != null && !title.isEmpty()) { columns.append("title,"); - values.append('\'').append(title.replaceAll("'", "''")).append("',"); + 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).collect(Collectors.joining(";"))).append("',"); + values.append('\'') + .append(Optional.of(artist) + .map(s -> s + .split(";")) + .stream() + .flatMap(Arrays::stream) + .map(String::trim) + .collect(Collectors + .joining(";"))) + .append("',"); } if (track != null && track[0] > 0) { @@ -707,15 +864,18 @@ public class LocalSongProvider implements SongProvider columns.append("duration,"); values.append(duration).append(","); } - if (albumTitle != null && !albumTitle.isEmpty()) + if (albumTitle != null && !albumTitle + .isEmpty()) { columns.append("album,"); - values.append('\'').append(albumTitle).append("',"); + values.append('\'') + .append(albumTitle) + .append("',"); } - + columns.append("mod"); values.append(file.lastModified()); - + columns.append(") VALUES "); values.append(");"); sql.append(columns); @@ -723,7 +883,7 @@ public class LocalSongProvider implements SongProvider state.executeUpdate(sql.toString()); } } - + updatedSongs++; triggerUpdateListeners(); } @@ -776,16 +936,16 @@ public class LocalSongProvider implements SongProvider } } } - + private class SongQuery extends RecursiveAction { private final boolean scan; - + SongQuery(boolean scan) { this.scan = scan; } - + @Override protected void compute() { @@ -794,7 +954,7 @@ public class LocalSongProvider implements SongProvider Album album; LocalSong song; int numAlbums = 0, numSongs = 0; - + try { logger.debug("Querying database."); @@ -806,21 +966,22 @@ public class LocalSongProvider implements SongProvider /* * 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';"); + result = state + .executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='LOCAL_ALBUMS';"); if (!result.next()) { logger.debug("Creating album table."); @@ -837,7 +998,8 @@ public class LocalSongProvider implements SongProvider } else { - result = state.executeQuery("SELECT * FROM LOCAL_ALBUMS;"); + result = state + .executeQuery("SELECT * FROM LOCAL_ALBUMS;"); while (result.next()) { album = albums.get(result.getString("album")); @@ -847,15 +1009,22 @@ public class LocalSongProvider implements SongProvider 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.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.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='LOCAL_SONGS';"); + result = state + .executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='LOCAL_SONGS';"); if (!result.next()) { logger.debug("Creating song table."); @@ -876,14 +1045,16 @@ public class LocalSongProvider implements SongProvider } else { - result = state.executeQuery("SELECT * FROM LOCAL_SONGS;"); + result = state + .executeQuery("SELECT * FROM LOCAL_SONGS;"); while (result.next()) { if (result.getString("file") == null) { continue; } - song = songs.get(new File(result.getString("file"))); + song = songs + .get(new File(result.getString("file"))); if (song == null) { song = new LocalSong(); @@ -893,11 +1064,17 @@ public class LocalSongProvider implements SongProvider 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.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")); + song.album = Optional + .ofNullable(result.getString("album")) + .map(albums::get) + .orElse(albums.get("Unknown")); numSongs++; } } @@ -912,7 +1089,7 @@ public class LocalSongProvider implements SongProvider updatedSongs = 0; totalUpdate = 0; triggerUpdateListeners(); - + if (scan) { LinkedList scanners = new LinkedList<>(); @@ -923,7 +1100,7 @@ public class LocalSongProvider implements SongProvider invokeAll(new ScanCompletion()); } } - + /** * Searches for all files and scans them * @@ -934,7 +1111,8 @@ public class LocalSongProvider implements SongProvider { if (dir.isDirectory()) { - for (File file : Objects.requireNonNullElse(dir.listFiles(), new File[0])) + for (File file : Objects + .requireNonNullElse(dir.listFiles(), new File[0])) { this.invokeFolder(file, scanners); } @@ -946,7 +1124,7 @@ public class LocalSongProvider implements SongProvider } } } - + private class ScanCompletion extends RecursiveAction { @Override @@ -954,13 +1132,13 @@ public class LocalSongProvider implements SongProvider { while (true) { - if (SongScanner.service.awaitQuiescence(60, TimeUnit.SECONDS)) + if (service.awaitQuiescence(60, TimeUnit.SECONDS)) { break; } } logger.debug("Scan complete. Researching database"); - SongScanner.currentFolder = ""; + currentFolder = ""; updatedSongs = 0; totalUpdate = 0; triggerUpdateListeners(); diff --git a/interface/src/main/java/edu/regis/universeplayer/player/Interface.java b/interface/src/main/java/edu/regis/universeplayer/player/Interface.java index 0f91453..b2d4a03 100644 --- a/interface/src/main/java/edu/regis/universeplayer/player/Interface.java +++ b/interface/src/main/java/edu/regis/universeplayer/player/Interface.java @@ -833,11 +833,13 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL this.controls.setUpdateProgress(updated, totalUpdate, updating); if (updated == totalUpdate || totalUpdate == 0) { - logger.debug("Resetting the song provider."); + Collection songs = SongProvider.INSTANCE.getSongs(); + logger.debug("Resetting the song provider with {} songs.", + songs.size()); /* * TODO - Add some way to get back to the current view, just updated */ - this.updateSongs(SongProvider.INSTANCE.getSongs()); + this.updateSongs(songs); } } diff --git a/interface/src/main/java/edu/regis/universeplayer/player/SongList.java b/interface/src/main/java/edu/regis/universeplayer/player/SongList.java index 0256e1a..c2b329f 100644 --- a/interface/src/main/java/edu/regis/universeplayer/player/SongList.java +++ b/interface/src/main/java/edu/regis/universeplayer/player/SongList.java @@ -5,15 +5,21 @@ package edu.regis.universeplayer.player; import com.wordpress.tips4java.ScrollablePanel; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import edu.regis.universeplayer.ClickListener; import edu.regis.universeplayer.data.Queue; import edu.regis.universeplayer.data.*; import javax.swing.*; + import java.awt.*; import java.awt.event.*; import java.util.List; import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; /** @@ -21,6 +27,11 @@ import java.util.stream.Collectors; */ public class SongList extends ScrollablePanel { + private static final Logger logger = LoggerFactory + .getLogger(SongList.class); + private static final ResourceBundle langs = ResourceBundle + .getBundle("lang.interface", Locale.getDefault()); + private Map> currentAlbums; private Map labelMap = new HashMap<>(); private Map artMap = new HashMap<>(); @@ -35,7 +46,7 @@ public class SongList extends ScrollablePanel SongProvider provider = SongProvider.INSTANCE; this.listAlbums(provider.getSongs()); - + this.setScrollableWidth(ScrollableSizeHint.FIT); this.setScrollableHeight(ScrollableSizeHint.STRETCH); this.setFocusTraversalPolicy(new SongListPolicy()); @@ -45,7 +56,8 @@ public class SongList extends ScrollablePanel public void focusGained(FocusEvent e) { int index = -1; - Component[] children = ((Container) e.getComponent()).getComponents(); + Component[] children = ((Container) e.getComponent()) + .getComponents(); for (int i = 0, l = children.length; index == -1 && i < l; i++) { if (children[i] == e.getOppositeComponent()) @@ -55,10 +67,11 @@ public class SongList extends ScrollablePanel } if (index == -1) { - artMap.keySet().stream().findFirst().ifPresent(albumInfo -> { - albumInfo.requestFocusInWindow(); - scrollRectToVisible(albumInfo.getBounds()); - }); + artMap.keySet().stream().findFirst() + .ifPresent(albumInfo -> { + albumInfo.requestFocusInWindow(); + scrollRectToVisible(albumInfo.getBounds()); + }); } } }); @@ -71,28 +84,27 @@ public class SongList extends ScrollablePanel */ public void listAlbums(Collection songs) { + 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; - int i = 0; - List songCollection; - JLabel songNum; - JButton songTitle; - + AtomicInteger i = new AtomicInteger(0); + this.labelMap.clear(); this.artMap.clear(); this.removeAll(); this.currentAlbums = albums; - for (Album album : albums.keySet()) - { - songCollection = albums.get(album); - + albums.keySet().stream().sorted().forEach((album) -> { + List songCollection = albums.get(album); + AlbumInfo albumInfo = new AlbumInfo(album); c.gridx = 0; - c.gridy = i; + c.gridy = i.get(); c.gridwidth = 1; c.gridheight = songCollection.size(); c.weightx = 0; @@ -113,10 +125,12 @@ public class SongList extends ScrollablePanel { inter = inter.getParent(); } - while (!(inter instanceof Interface) && inter.getParent() != null); + while (!(inter instanceof Interface) && inter + .getParent() != null); if (inter instanceof Interface) { - ((Interface) inter).updateSongs(SongProvider.INSTANCE.getSongsFromAlbum(albumInfo.album)); + ((Interface) inter).updateSongs(SongProvider.INSTANCE + .getSongsFromAlbum(albumInfo.album)); } } }); @@ -128,10 +142,17 @@ public class SongList extends ScrollablePanel { inter = inter.getParent(); } - while (!(inter instanceof Interface) && inter.getParent() != null); + 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())); + ((Interface) inter) + .updateCollections(CollectionType.album, Arrays + .stream(albumInfo.album.artists) + .flatMap(s -> SongProvider.INSTANCE + .getAlbumsFromArtist(s) + .stream()) + .collect(Collectors.toList())); } } }); @@ -143,10 +164,16 @@ public class SongList extends ScrollablePanel { inter = inter.getParent(); } - while (!(inter instanceof Interface) && inter.getParent() != null); + 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())); + ((Interface) inter) + .updateCollections(CollectionType.album, Arrays + .stream(albumInfo.album.genres) + .flatMap(s -> SongProvider.INSTANCE + .getAlbumsFromGenre(s).stream()) + .collect(Collectors.toList())); } } }); @@ -158,10 +185,13 @@ public class SongList extends ScrollablePanel { inter = inter.getParent(); } - while (!(inter instanceof Interface) && inter.getParent() != null); + while (!(inter instanceof Interface) && inter + .getParent() != null); if (inter instanceof Interface) { - ((Interface) inter).updateCollections(CollectionType.album, SongProvider.INSTANCE.getAlbumsFromYear(albumInfo.album.year)); + ((Interface) inter) + .updateCollections(CollectionType.album, SongProvider.INSTANCE + .getAlbumsFromYear(albumInfo.album.year)); } } }); @@ -169,13 +199,15 @@ public class SongList extends ScrollablePanel this.artMap.put(albumInfo, album); JButton firstSong = null; - + + JLabel songNum; + JButton songTitle; for (Song song : songCollection) { songNum = new JLabel(String.valueOf(song.trackNum)); songNum.setFocusable(false); c.gridx = 1; - c.gridy = i; + c.gridy = i.get(); c.gridheight = 1; c.weightx = 0; c.anchor = GridBagConstraints.NORTHEAST; @@ -184,6 +216,13 @@ public class SongList extends ScrollablePanel this.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)); @@ -196,11 +235,12 @@ public class SongList extends ScrollablePanel public void actionPerformed(ActionEvent e) { Queue.getInstance().add(song); - Queue.getInstance().skipToSong(Queue.getInstance().size() - 1); + Queue.getInstance() + .skipToSong(Queue.getInstance().size() - 1); } }); c.gridx = 2; - c.gridy = i; + c.gridy = i.get(); c.weightx = 1.0; c.anchor = GridBagConstraints.NORTHWEST; c.insets = new Insets(0, 10, 0, 0); @@ -228,25 +268,27 @@ public class SongList extends ScrollablePanel { if (e.getKeyCode() == KeyEvent.VK_ENTER) { - Queue.getInstance().addAll(finalSongCollection1); + Queue.getInstance() + .addAll(finalSongCollection1); } } }); } - i++; + i.getAndIncrement(); } // this.add(new JLabel(new ImageIcon(this.getClass().getResource("/gui/icons/defaultart.png"), "Default")), c); - + c.gridx = 0; - c.gridy = i++; + c.gridy = i.getAndIncrement(); c.gridwidth = 3; c.anchor = GridBagConstraints.NORTH; this.add(new JSeparator(SwingConstants.HORIZONTAL), c); - - i++; - } + + i.getAndIncrement(); + }); + logger.debug("Song list built"); } - + private class SongListPolicy extends FocusTraversalPolicy { @Override @@ -254,7 +296,8 @@ public class SongList extends ScrollablePanel { Component[] children = aContainer.getComponents(); int index = -1; - for (int i = 0, l = aContainer.getComponentCount(); index == -1 && i < l; i++) + for (int i = 0, l = aContainer + .getComponentCount(); index == -1 && i < l; i++) { if (children[i] == aComponent) { @@ -265,7 +308,8 @@ public class SongList extends ScrollablePanel { if (aComponent instanceof AlbumInfo) { - for (int i = index + 1, l = aContainer.getComponentCount(); i < l; i++) + for (int i = index + 1, l = aContainer + .getComponentCount(); i < l; i++) { if (children[i] instanceof AlbumInfo) { @@ -276,7 +320,8 @@ public class SongList extends ScrollablePanel } else if (aComponent instanceof JButton) { - for (int i = index + 1, l = aContainer.getComponentCount(); i < l; i++) + for (int i = index + 1, l = aContainer + .getComponentCount(); i < l; i++) { if (children[i].isFocusable()) { @@ -288,13 +333,14 @@ public class SongList extends ScrollablePanel } return null; } - + @Override public Component getComponentBefore(Container aContainer, Component aComponent) { Component[] children = aContainer.getComponents(); int index = -1; - for (int i = 0, l = aContainer.getComponentCount(); index == -1 && i < l; i++) + for (int i = 0, l = aContainer + .getComponentCount(); index == -1 && i < l; i++) { if (children[i] == aComponent) { @@ -328,22 +374,26 @@ public class SongList extends ScrollablePanel } return null; } - + @Override public Component getFirstComponent(Container aContainer) { - Component comp = Arrays.stream(aContainer.getComponents()).filter(a -> a instanceof AlbumInfo).findFirst().orElse(null); + Component comp = Arrays.stream(aContainer.getComponents()) + .filter(a -> a instanceof AlbumInfo) + .findFirst().orElse(null); if (comp != null) { scrollRectToVisible(comp.getBounds()); } return comp; } - + @Override public Component getLastComponent(Container aContainer) { - Component[] matching = Arrays.stream(aContainer.getComponents()).filter(a -> a instanceof JButton).toArray(Component[]::new); + Component[] matching = Arrays.stream(aContainer.getComponents()) + .filter(a -> a instanceof JButton) + .toArray(Component[]::new); if (matching.length > 0) { scrollRectToVisible(matching[matching.length - 1].getBounds()); @@ -354,7 +404,7 @@ public class SongList extends ScrollablePanel return null; } } - + @Override public Component getDefaultComponent(Container aContainer) {