Improves the caching strategy.

Now much more performant.
This commit is contained in:
Markil3
2021-08-02 15:08:10 -07:00
parent 69a1d9cbf4
commit 8386c9575c
2 changed files with 490 additions and 414 deletions

View File

@@ -26,7 +26,7 @@ public class LocalSongProvider implements SongProvider<LocalSong>
private final File source;
private Connection db;
private final HashSet<LocalSong> songs = new HashSet<>();
private final HashMap<File, LocalSong> songs = new HashMap<>();
private final HashMap<String, Album> albums = new HashMap<>();
/**
* A cache of all song artists.
@@ -152,6 +152,245 @@ public class LocalSongProvider implements SongProvider<LocalSong>
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)
{
Connection dbL = null;
this.source = source;
if (this.source == null || !this.source.isDirectory())
{
throw new IllegalArgumentException("File source must be existing directory");
}
this.getDb();
getSongCache();
}
private void getSongCache()
{
SongScanner.service.submit(new SongQuery(true));
}
/**
* 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<LocalSong> 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<LocalSong> 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<LocalSong> 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 SongScanner.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()));
}
private class SongScanner extends RecursiveAction
{
private static final ForkJoinPool service = new ForkJoinPool();
@@ -183,8 +422,8 @@ public class LocalSongProvider implements SongProvider<LocalSong>
Integer[] track = null;
Integer[] disc = null;
LocalSong song;
Album album;
Statement state = null;
ResultSet result;
try
{
@@ -204,7 +443,26 @@ public class LocalSongProvider implements SongProvider<LocalSong>
{
try
{
state = getDb().createStatement();
result = state.executeQuery("SELECT mod FROM local_songs WHERE file='" + this.file.getAbsolutePath() + "';");
if (result.next())
{
if (result.getLong(1) >= this.file.lastModified())
{
/*
* No modifications needed
*/
updatedSongs++;
triggerUpdateListeners();
return;
}
else
{
state.executeUpdate("UPDATE local_songs SET mod = " + this.file.lastModified() + " WHERE file='" + this.file.getAbsolutePath() + "';");
}
}
currentFolder = file.getPath();
triggerUpdateListeners();
codec = null;
process = Runtime.getRuntime().exec(new String[] {"ffprobe", "-hide_banner", file.getAbsolutePath()});
process.waitFor();
@@ -351,82 +609,151 @@ public class LocalSongProvider implements SongProvider<LocalSong>
/*
* Update album information.
*/
synchronized (albums)
result = state.executeQuery("SELECT album FROM local_albums WHERE album='" + albumTitle + "';");
if (!result.next())
{
album = albums.get(albumTitle);
if (album == null)
{
album = new Album();
album.name = albumTitle;
albums.put(albumTitle, album);
state.executeUpdate("INSERT INTO local_albums (album) VALUES ('" + albumTitle + "');");
}
}
if (album.artists == null && albumArtist != null)
result = state.executeQuery("SELECT artists FROM local_albums WHERE album='" + albumTitle + "';");
if (result.getString("artists") == null && albumArtist != null)
{
album.artists = Arrays.stream(albumArtist.split(";")).map(String::trim).toArray(String[]::new);
synchronized (albumArtists)
{
albumArtists.addAll(Arrays.asList(album.artists));
state.executeUpdate("UPDATE local_albums SET artists='" + Arrays.stream(albumArtist.split(";")).map(String::trim).collect(Collectors.joining(";")) + "' WHERE album='" + albumTitle + "';");
}
}
if (album.genres == null && genre != null)
// TODO - Can we get year metadata?
result = state.executeQuery("SELECT genres FROM local_albums WHERE album='" + albumTitle + "';");
if (result.getString("genres") == null && genre != null)
{
album.genres = Arrays.stream(genre.split(";")).map(String::trim).toArray(String[]::new);
synchronized (genres)
{
genres.addAll(Arrays.asList(album.genres));
state.executeUpdate("UPDATE local_albums SET genres='" + Arrays.stream(genre.split(";")).map(String::trim).collect(Collectors.joining(";")) + "' WHERE album='" + albumTitle + "';");
}
}
if (album.totalTracks == 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)
{
album.totalTracks = track[1];
state.executeUpdate("UPDATE local_albums SET tracks=" + track[1] + " WHERE album='" + albumTitle + "';");
}
if (album.totalDiscs == 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)
{
album.totalDiscs = disc[1];
state.executeUpdate("UPDATE local_albums SET tracks=" + disc[1] + " WHERE album='" + albumTitle + "';");
}
/*
* Create the song
*/
song = new LocalSong();
song.file = file.getAbsoluteFile();
song.type = type;
song.codec = codec;
song.album = album;
if (title != null)
result = state.executeQuery("SELECT title FROM local_songs WHERE file='" + file.getAbsolutePath() + "';");
if (result.next())
{
song.title = title;
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).append("', ");
}
if (artist != null)
else
{
song.artists = Arrays.stream(artist.split(";")).map(String::trim).toArray(String[]::new);
synchronized (artists)
{
artists.addAll(Arrays.asList(song.artists));
sql.append("title='").append(file.getName()).append("', ");
}
}
if (disc != null && disc[0] > 0)
if (artist != null && !artist.isEmpty())
{
song.disc = disc[0];
sql.append("artists='").append(Optional.of(artist).map(s -> s.split(";")).stream().flatMap(Arrays::stream).map(String::trim).collect(Collectors.joining(";"))).append("', ");
}
else
{
sql.append("artists=NULL, ");
}
if (track != null && track[0] > 0)
{
song.trackNum = track[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()).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()).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).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("',");
}
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)
{
song.duration = duration;
columns.append("duration,");
values.append(duration).append(",");
}
if (albumTitle != null && !albumTitle.isEmpty())
{
columns.append("album,");
values.append('\'').append(albumTitle).append("',");
}
logger.debug("Caching song {} ({})", song, song.file);
columns.append("mod");
values.append(file.lastModified());
columns.append(") VALUES ");
values.append(");");
sql.append(columns);
sql.append(values);
state.executeUpdate(sql.toString());
}
updatedSongs++;
triggerUpdateListeners();
synchronized (songs)
{
songs.add(song);
}
}
else
{
@@ -438,10 +765,18 @@ public class LocalSongProvider implements SongProvider<LocalSong>
logger.trace("Could not find codec for {}", 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
{
@@ -470,42 +805,18 @@ public class LocalSongProvider implements SongProvider<LocalSong>
}
}
private Connection getDb()
private class SongQuery extends RecursiveAction
{
SQLWarning warning;
try
private final boolean scan;
SongQuery(boolean scan)
{
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());
this.db.setAutoCommit(false);
}
warning = this.db.getWarnings();
while (warning != null)
{
logger.warn("SQL Warning: ", warning);
warning = warning.getNextWarning();
}
}
catch (SQLException | ClassNotFoundException e)
{
logger.error("Could not store caching DIR.");
}
return this.db;
this.scan = scan;
}
public LocalSongProvider(File source)
@Override
protected void compute()
{
Connection dbL = null;
this.source = source;
if (this.source == null || !this.source.isDirectory())
{
throw new IllegalArgumentException("File source must be existing directory");
}
this.getDb();
SongScanner.service.submit(() -> {
Statement state;
ResultSet result;
Album album;
@@ -514,24 +825,75 @@ public class LocalSongProvider implements SongProvider<LocalSong>
try
{
logger.debug("Querying database.");
state = this.getDb().createStatement();
/*
* Check if the table exists
*/
state = 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())
{
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");
albums.put(album.name, album);
}
}
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())
{
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");
@@ -540,52 +902,33 @@ public class LocalSongProvider implements SongProvider<LocalSong>
song.disc = result.getInt("disc");
song.duration = result.getLong("duration");
song.album = Optional.ofNullable(result.getString("album")).map(albums::get).orElse(null);
songs.add(song);
}
result.close();
}
state.close();
}
catch (SQLException e)
{
logger.error("Could not query SQL database.", e);
}
if (songs.isEmpty())
{
logger.debug("No songs within database. Scanning...");
totalUpdate = 1;
updateSongs();
}
else
{
try
{
this.getDb().commit();
this.getDb().close();
logger.debug("Query complete.");
updatedSongs = 0;
totalUpdate = 0;
triggerUpdateListeners();
}
catch (SQLException e)
if (scan)
{
logger.error("Could not close database.", e);
logger.debug("Scanning for changes...");
totalUpdate = 1;
invokeAll(new SongScanner(source), new ScanCompletion());
}
}
});
}
/**
* Scans the local file system for song files.
*/
private void updateSongs()
private class ScanCompletion extends RecursiveAction
{
@Override
protected void compute()
{
SongScanner.service.submit(new SongScanner(source));
/*
* Resets the song scanner when ready.
*/
SongScanner.service.submit(() -> {
Statement stat;
ResultSet tableResult;
while (true)
{
if (SongScanner.service.awaitQuiescence(60, TimeUnit.SECONDS))
@@ -597,276 +940,7 @@ public class LocalSongProvider implements SongProvider<LocalSong>
updatedSongs = 0;
totalUpdate = 0;
triggerUpdateListeners();
/*
* Update the SQL database.
*/
try
{
logger.debug("Updating local song database.");
stat = this.getDb().createStatement();
tableResult = stat.executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='LOCAL_SONGS';");
logger.debug("Table result: {}", tableResult.getCursorName());
if (!tableResult.next())
{
logger.debug("Creating song table.");
/*
* Create the table
*/
stat.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);");
invokeAll(new SongQuery(false));
}
else
{
logger.debug("Clearing song table.");
stat.executeUpdate("DELETE FROM LOCAL_SONGS;");
}
tableResult = stat.executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='LOCAL_ALBUMS';");
if (!tableResult.next())
{
logger.debug("Creating album table.");
/*
* Create the table
*/
stat.executeUpdate("CREATE TABLE LOCAL_ALBUMS" +
"(ALBUM TEXT PRIMARY KEY NOT NULL," +
"ARTISTS TEXT," +
"YEAR INTEGER," +
"GENRES TEXT," +
"TRACKS INTEGER," +
"DISCS INTEGER);");
}
else
{
logger.debug("Clearing album table.");
stat.executeUpdate("DELETE FROM LOCAL_ALBUMS;");
}
tableResult.close();
for (Album album : this.albums.values())
{
stat.executeUpdate("INSERT INTO LOCAL_ALBUMS (ALBUM,ARTISTS,YEAR,GENRES,TRACKS,DISCS) " +
"VALUES ('" + album.name + "', '" + String.join(";", album.artists) + "', " + album.year + ", '" + String.join(";", album.genres) + "', " + album.totalTracks + ", " + album.totalDiscs + ");");
}
for (LocalSong song : this.songs)
{
stat.executeUpdate("INSERT INTO LOCAL_SONGS (FILE,CODEC,TYPE,TITLE,ARTISTS,TRACK,DISC,DURATION,ALBUM) " +
"VALUES ('" + song.file.getAbsolutePath() + "', '" + song.codec + "', '" + song.type + "', '" + song.title + "', '" + String.join(";", song.artists) + "', " + song.trackNum + ", " + song.disc + ", " + song.duration + ", '" + song.album.name + "');");
}
stat.close();
this.getDb().commit();
this.getDb().close();
logger.debug("Cache updated.");
}
catch (SQLException e)
{
logger.error("Error in saving SQLite database", e);
}
});
}
/**
* 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<LocalSong> getSongs()
{
synchronized (this.songs)
{
return Collections.unmodifiableCollection(this.songs);
}
}
/**
* 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<LocalSong> getSongsFromAlbum(Album album)
{
synchronized (this.songs)
{
return this.songs.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<LocalSong> getSongsFromArtist(String artist)
{
synchronized (this.songs)
{
return this.songs.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 SongScanner.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()));
}
}

View File

@@ -198,6 +198,8 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
this.addWindowListener(this);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.updateSongs(SongProvider.INSTANCE.getSongs());
}
@Override