Adds the ability to add internet songs to the library.

This commit is contained in:
Markil3
2021-08-15 18:18:02 -06:00
parent 76594b279c
commit b546c81fa1
7 changed files with 714 additions and 35 deletions

View File

@@ -0,0 +1,49 @@
/*
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
*/
package edu.regis.universeplayer.data;
import edu.regis.universeplayer.player.Interface;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLWarning;
/**
* Manages the application song database
*/
public class DatabaseManager
{
private static final Logger logger = LoggerFactory.getLogger(DatabaseManager.class);
private static Connection db;
public static synchronized Connection getDb()
{
SQLWarning warning;
try
{
if (db == null || db.isClosed())
{
Class.forName("org.sqlite.JDBC");
db = DriverManager.getConnection("jdbc:sqlite:" + new File(Interface.getDataDir().getAbsolutePath(), "universalmusic.db").getAbsolutePath());
}
warning = db.getWarnings();
while (warning != null)
{
logger.warn("SQL Warning: ", warning);
warning = warning.getNextWarning();
}
db.clearWarnings();
}
catch (SQLException | ClassNotFoundException e)
{
logger.error("Could not store caching DIR.");
}
return db;
}
}

View File

@@ -0,0 +1,469 @@
/*
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
*/
package edu.regis.universeplayer.data;
import edu.regis.universeplayer.browser.InternetSong;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
public class InternetSongProvider implements SongProvider<InternetSong>
{
private static final Logger logger = LoggerFactory.getLogger(InternetSongProvider.class);
private static final ExecutorService service = Executors.newSingleThreadExecutor();
private static InternetSongProvider INSTANCE;
public static InternetSongProvider getInstance()
{
if (INSTANCE == null)
{
INSTANCE = new InternetSongProvider();
}
return INSTANCE;
}
private final HashMap<URL, InternetSong> songs = new HashMap<>();
private final HashMap<String, Album> albums = new HashMap<>();
/**
* A cache of all song artists.
*/
private final HashSet<String> artists = new HashSet<>();
/**
* A cache of all album genres.
*/
private final HashSet<String> genres = new HashSet<>();
/**
* A cache of all album artists.
*/
private final HashSet<String> albumArtists = new HashSet<>();
/**
* A cache of all album release years.
*/
private final HashSet<Integer> years = new HashSet<>();
private int updatedSongs;
private int totalUpdate;
private final LinkedList<UpdateListener> listeners = new LinkedList<>();
private InternetSongProvider()
{
this.getSongCache();
}
private void getSongCache()
{
service.submit(new SongQuery());
}
/**
* Obtains all albums within the collection.
*
* @return A list of albums.
*/
@Override
public Collection<Album> getAlbums()
{
return this.albums.values();
}
/**
* Obtains all songs within the collection.
*
* @return A list of songs.
*/
@Override
public Collection<InternetSong> getSongs()
{
synchronized (this.songs)
{
return Collections.unmodifiableCollection(this.songs.values());
}
}
/**
* Obtains a list of all artists.
*
* @return All artists.
*/
@Override
public Collection<String> getArtists()
{
return this.artists;
}
/**
* Obtains a list of all album artists.
*
* @return All album artists.
*/
@Override
public Collection<String> getAlbumArtists()
{
return this.albumArtists;
}
/**
* Obtains a list of all genres.
*
* @return All genres.
*/
@Override
public Collection<String> getGenres()
{
return this.genres;
}
/**
* Obtains a list of all years that have albums.
*
* @return All years.
*/
@Override
public Collection<Integer> getYears()
{
return this.years;
}
/**
* Obtains all songs from an album.
*
* @param album - The album to obtain
* @return All songs from the requested album, or null if that album is not in the database.
*/
@Override
public Collection<InternetSong> getSongsFromAlbum(Album album)
{
synchronized (this.songs)
{
return this.songs.values().stream().filter(song -> song.album.equals(album)).collect(Collectors.toUnmodifiableSet());
}
}
/**
* Obtains all songs written by a given artist.
*
* @param artist - The artist to search for
* @return A list of all songs from the specified artist, or null if that artist is not in the
* database.
*/
@Override
public Collection<InternetSong> getSongsFromArtist(String artist)
{
synchronized (this.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.albums)
{
return this.albums.get(name);
}
}
/**
* Obtains all albums that were written by a certain artist.
*
* @param artist - The artist to search for.
* @return - The collection on matching albums.
*/
@Override
public Collection<Album> getAlbumsFromArtist(String artist)
{
synchronized (this.albums)
{
return this.albums.values().stream().filter(album -> Arrays.asList(album.artists).contains(artist)).collect(Collectors.toUnmodifiableSet());
}
}
/**
* Obtains all albums that match a certain genre
*
* @param genre - The genre to search for.
* @return - The collection on matching albums.
*/
@Override
public Collection<Album> getAlbumsFromGenre(String genre)
{
synchronized (this.albums)
{
return this.albums.values().stream().filter(album -> Arrays.asList(album.genres).contains(genre)).collect(Collectors.toUnmodifiableSet());
}
}
/**
* Obtains all albums that were released a certain year.
*
* @param year - The year to search for.
* @return - The collection on matching albums.
*/
@Override
public Collection<Album> getAlbumsFromYear(int year)
{
synchronized (this.albums)
{
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 "";
}
@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<InternetSong> addSong(URL url, String title, String albumName, String artists, String genres)
{
return service.submit(() -> {
Album album = albums.get(albumName);
Statement state = DatabaseManager.getDb().createStatement();
if (album == null)
{
album = new Album();
album.name = albumName;
albums.put(albumName, album);
synchronized (DatabaseManager.getDb())
{
state.executeUpdate("INSERT INTO internet_albums (album) VALUES ('" + albumName + "');");
}
}
album.artists = Arrays.stream(artists.split(";")).map(String::trim).toArray(String[]::new);
album.genres = Arrays.stream(genres.split(";")).map(String::trim).toArray(String[]::new);
synchronized (DatabaseManager.getDb())
{
state.executeUpdate("UPDATE internet_albums SET artists='" + String.join(";", album.artists) + "' WHERE album='" + albumName + "';");
state.executeUpdate("UPDATE internet_albums SET genres='" + String.join(";", album.genres) + "' WHERE album='" + albumName + "';");
}
InternetSong song = new InternetSong();
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()
{
}
@Override
public void run()
{
Statement state;
ResultSet result;
Album album;
InternetSong song;
int numAlbums = 0, numSongs = 0;
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();
}
}
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;
triggerUpdateListeners();
}
}
}

View File

@@ -24,7 +24,6 @@ public class LocalSongProvider implements SongProvider<LocalSong>
private static final HashSet<String> codecs = new HashSet<>(); private static final HashSet<String> codecs = new HashSet<>();
private final File source; private final File source;
private Connection db;
private final HashMap<File, LocalSong> songs = new HashMap<>(); private final HashMap<File, LocalSong> songs = new HashMap<>();
private final HashMap<String, Album> albums = new HashMap<>(); private final HashMap<String, Album> albums = new HashMap<>();
@@ -152,31 +151,6 @@ public class LocalSongProvider implements SongProvider<LocalSong>
return codecs; return codecs;
} }
private Connection getDb()
{
SQLWarning warning;
try
{
if (this.db == null || this.db.isClosed())
{
Class.forName("org.sqlite.JDBC");
this.db = DriverManager.getConnection("jdbc:sqlite:" + new File(Interface.getDataDir().getAbsolutePath(), "universalmusic.db").getAbsolutePath());
}
warning = this.db.getWarnings();
while (warning != null)
{
logger.warn("SQL Warning: ", warning);
warning = warning.getNextWarning();
}
this.db.clearWarnings();
}
catch (SQLException | ClassNotFoundException e)
{
logger.error("Could not store caching DIR.");
}
return this.db;
}
public LocalSongProvider(File source) public LocalSongProvider(File source)
{ {
Connection dbL = null; Connection dbL = null;
@@ -185,8 +159,6 @@ public class LocalSongProvider implements SongProvider<LocalSong>
{ {
throw new IllegalArgumentException("File source must be existing directory"); throw new IllegalArgumentException("File source must be existing directory");
} }
this.getDb();
getSongCache(); getSongCache();
} }
@@ -434,9 +406,9 @@ public class LocalSongProvider implements SongProvider<LocalSong>
{ {
try try
{ {
synchronized (db) synchronized (DatabaseManager.getDb())
{ {
state = getDb().createStatement(); state = DatabaseManager.getDb().createStatement();
result = state.executeQuery("SELECT mod FROM local_songs WHERE file='" + this.file.getAbsolutePath().replaceAll("'", "''") + "';"); result = state.executeQuery("SELECT mod FROM local_songs WHERE file='" + this.file.getAbsolutePath().replaceAll("'", "''") + "';");
if (result.next()) if (result.next())
{ {
@@ -603,7 +575,7 @@ public class LocalSongProvider implements SongProvider<LocalSong>
/* /*
* Update album information. * Update album information.
*/ */
synchronized (db) synchronized (DatabaseManager.getDb())
{ {
/* /*
* This part in particular is prone to thread-safety issues. * This part in particular is prone to thread-safety issues.
@@ -829,7 +801,7 @@ public class LocalSongProvider implements SongProvider<LocalSong>
/* /*
* Check if the table exists * Check if the table exists
*/ */
synchronized (db) synchronized (DatabaseManager.getDb())
{ {
/* /*
* Make sure that a "null" album is available * Make sure that a "null" album is available
@@ -847,7 +819,7 @@ public class LocalSongProvider implements SongProvider<LocalSong>
albums.put("Unknown", albums.get(null)); albums.put("Unknown", albums.get(null));
} }
state = getDb().createStatement(); 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()) if (!result.next())
{ {

View File

@@ -18,7 +18,7 @@ public interface SongProvider<T extends Song>
/** /**
* A SongProvider instance designed to * A SongProvider instance designed to
*/ */
SongProvider<Song> INSTANCE = new CompiledSongProvider(new LocalSongProvider(new File(System.getProperty("user.home"), "Music"))); SongProvider<Song> INSTANCE = new CompiledSongProvider(new LocalSongProvider(new File(System.getProperty("user.home"), "Music")), InternetSongProvider.getInstance());
/** /**
* Obtains all albums within the collection. * Obtains all albums within the collection.

View File

@@ -71,6 +71,10 @@ public class Collections extends JPanel
this.add(new JLabel("\u23AF".repeat(6))); this.add(new JLabel("\u23AF".repeat(6)));
this.add(label = this.createButton("Playlists")); this.add(label = this.createButton("Playlists"));
label.setMnemonic('P'); label.setMnemonic('P');
this.add(new JLabel("\u23AF".repeat(6)));
this.add(label = this.createButton("Add External Song"));
label.setMnemonic('E');
label.addActionListener(mouseEvent -> this.addNewSong());
addMouseListener(new MouseAdapter() addMouseListener(new MouseAdapter()
{ {
@@ -106,6 +110,16 @@ public class Collections extends JPanel
}); });
} }
private void addNewSong()
{
Container parent = this.getParent();
while (!(parent instanceof JFrame) && parent != null)
{
parent = parent.getParent();
}
new InternetSongDialog((JFrame) parent).setVisible(true);
}
private JButton createButton(String text) private JButton createButton(String text)
{ {
JButton label = new JButton(text); JButton label = new JButton(text);

View File

@@ -0,0 +1,172 @@
/*
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
*/
package edu.regis.universeplayer.player;
import edu.regis.universeplayer.browser.InternetSong;
import edu.regis.universeplayer.data.InternetSongProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import java.awt.*;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
/**
* This dialog is used to add songs
*/
public class InternetSongDialog extends JDialog
{
private static final Logger logger = LoggerFactory.getLogger(InternetSongDialog.class);
private final JTextField urlBox;
private final JTextField titleBox;
private final JTextField albumBox;
private final JTextField artistBox;
private final JTextField genreBox;
public InternetSongDialog(Frame owner)
{
super(owner, true);
JLabel label;
GridBagConstraints c = new GridBagConstraints();
this.getContentPane().setLayout(new GridBagLayout());
int y = 0;
c.gridx = 0;
c.gridy = y;
c.gridwidth = 2;
c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.WEST;
c.insets = new Insets(0, 2, 5, 2);
this.getContentPane().add(label = new JLabel("URL"), c);
this.urlBox = new JTextField();
this.urlBox.addFocusListener(new FocusAdapter()
{
@Override
public void focusLost(FocusEvent e)
{
try
{
new URL(((JTextField) e.getSource()).getText());
((JTextField) e.getSource()).setForeground(Color.BLACK);
}
catch (MalformedURLException ex)
{
((JTextField) e.getSource()).requestFocusInWindow();
((JTextField) e.getSource()).setForeground(Color.RED);
}
}
});
c.gridy = ++y;
this.getContentPane().add(this.urlBox, c);
label.setLabelFor(this.urlBox);
c.gridx = 0;
c.gridy = ++y;
this.getContentPane().add(label = new JLabel("Song Title"), c);
this.titleBox = new JTextField();
c.gridy = ++y;
this.getContentPane().add(this.titleBox, c);
label.setLabelFor(this.titleBox);
c.gridx = 0;
c.gridy = ++y;
this.getContentPane().add(label = new JLabel("Album"), c);
this.albumBox = new JTextField();
c.gridy = ++y;
this.getContentPane().add(this.albumBox, c);
label.setLabelFor(this.albumBox);
c.gridx = 0;
c.gridy = ++y;
this.getContentPane().add(label = new JLabel("Artists"), c);
this.artistBox = new JTextField();
c.gridy = ++y;
this.getContentPane().add(this.artistBox, c);
label.setLabelFor(this.artistBox);
c.gridx = 0;
c.gridy = ++y;
this.getContentPane().add(label = new JLabel("Genre"), c);
this.genreBox = new JTextField();
c.gridy = ++y;
this.getContentPane().add(this.genreBox, c);
label.setLabelFor(this.genreBox);
c.gridwidth = 1;
c.gridx = 0;
c.gridy = ++y;
JButton cancelButton = new JButton("Cancel");
cancelButton.addActionListener(e -> this.dispose());
this.getContentPane().add(cancelButton, c);
c.gridx = 1;
JButton addButton = new JButton("Add");
addButton.addActionListener(e -> this.confirm());
this.getContentPane().add(addButton, c);
this.pack();
}
private void confirm()
{
URL url;
if (this.urlBox.getText().isEmpty())
{
this.urlBox.requestFocusInWindow();
this.urlBox.setForeground(Color.RED);
return;
}
try
{
url = new URL(this.urlBox.getText());
}
catch (MalformedURLException e)
{
this.urlBox.requestFocusInWindow();
return;
}
for (Component comp: this.getContentPane().getComponents())
{
comp.setEnabled(false);
}
SwingUtilities.invokeLater(() -> {
Future<InternetSong> future = InternetSongProvider.getInstance().addSong(url, this.titleBox.getText(), this.albumBox.getText(), this.artistBox.getText(), this.genreBox.getText());
InternetSong song = null;
try
{
song = future.get();
}
catch (InterruptedException | ExecutionException e)
{
logger.error("Could not evaluate song", e);
JOptionPane.showMessageDialog(InternetSongDialog.this, e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
finally
{
if (song != null)
{
dispose();
}
else
{
for (Component comp: this.getContentPane().getComponents())
{
comp.setEnabled(true);
}
}
}
});
}
}

View File

@@ -290,9 +290,12 @@ public class PlayerControls extends JPanel implements Queue.SongChangeListener,
public void onSongChange(Queue queue) public void onSongChange(Queue queue)
{ {
if (this.currentSong != null) if (this.currentSong != null)
{
if (this.currentPlayer != null)
{ {
this.currentPlayer.stopSong(); this.currentPlayer.stopSong();
} }
}
this.currentSong = queue.getCurrentSong(); this.currentSong = queue.getCurrentSong();
this.playButton.setIcon(PLAY_ICON); this.playButton.setIcon(PLAY_ICON);
if (this.currentSong != null) if (this.currentSong != null)