/* * 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 { 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 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() { this.getSongCache(); } private void getSongCache() { service.submit(new SongQuery()); } /** * Obtains all albums within the collection. * * @return A list of albums. */ @Override public Collection getAlbums() { return this.albums.values(); } /** * Obtains all songs within the collection. * * @return A list of songs. */ @Override public Collection getSongs() { synchronized (this.songs) { return Collections.unmodifiableCollection(this.songs.values()); } } /** * Obtains a list of all artists. * * @return All artists. */ @Override public Collection getArtists() { return this.artists; } /** * Obtains a list of all album artists. * * @return All album artists. */ @Override public Collection getAlbumArtists() { return this.albumArtists; } /** * Obtains a list of all genres. * * @return All genres. */ @Override public Collection getGenres() { return this.genres; } /** * Obtains a list of all years that have albums. * * @return All years. */ @Override public Collection 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 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 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 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 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 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 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(); } } }