Lets the interface cache local song/album data

This will let the interface avoid a system scan every time, improving performance.
This commit is contained in:
Markil3
2021-08-02 13:02:33 -07:00
parent bc327e8ab7
commit 69a1d9cbf4
5 changed files with 307 additions and 24 deletions

View File

@@ -21,6 +21,8 @@ dependencies {
implementation 'org.apache.logging.log4j:log4j-slf4j-impl:2.13.3'
implementation 'com.google.code.gson:gson:2.8.7'
implementation 'net.harawata:appdirs:1.2.1'
implementation 'org.xerial:sqlite-jdbc:3.36.0.1'
implementation 'com.googlecode.soundlibs:jlayer:1.0.1.4'
implementation 'com.mpatric:mp3agic:0.9.1'
implementation project(":libwave")

View File

@@ -4,15 +4,15 @@
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.io.IOException;
import java.sql.*;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -24,6 +24,7 @@ public class LocalSongProvider implements SongProvider<LocalSong>
private static final HashSet<String> codecs = new HashSet<>();
private final File source;
private Connection db;
private final HashSet<LocalSong> songs = new HashSet<>();
private final HashMap<String, Album> albums = new HashMap<>();
@@ -151,12 +152,11 @@ public class LocalSongProvider implements SongProvider<LocalSong>
return codecs;
}
private class SongScanner implements Runnable
private class SongScanner extends RecursiveAction
{
private static final int serviceThreads = Math.max(Runtime.getRuntime().availableProcessors() - 2, 1);
private static final ExecutorService service = Executors.newFixedThreadPool(serviceThreads);
private static final ForkJoinPool service = new ForkJoinPool();
private static String currentFolder;
private final File file;
SongScanner(File folder)
@@ -165,7 +165,7 @@ public class LocalSongProvider implements SongProvider<LocalSong>
}
@Override
public void run()
public void compute()
{
Process process;
String line;
@@ -191,12 +191,11 @@ public class LocalSongProvider implements SongProvider<LocalSong>
if (file.isDirectory())
{
totalUpdate--;
for (File subFile : Objects.requireNonNull(file.listFiles()))
{
totalUpdate++;
triggerUpdateListeners();
service.submit(new SongScanner(subFile));
}
List<SongScanner> tasks = Arrays.stream(Objects.requireNonNullElse(file.listFiles(), new File[0]))
.map(SongScanner::new).collect(Collectors.toList());
totalUpdate += tasks.size();
triggerUpdateListeners();
invokeAll(tasks);
}
else if (file.getName().lastIndexOf(".") < file.getName().length() - 1)
{
@@ -421,7 +420,7 @@ public class LocalSongProvider implements SongProvider<LocalSong>
}
logger.debug("Caching song {} ({})", song, song.file);
updatedSongs++;
triggerUpdateListeners();
synchronized (songs)
@@ -471,37 +470,207 @@ public class LocalSongProvider implements SongProvider<LocalSong>
}
}
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());
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;
}
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");
}
// TODO - Add some sort of caching system
totalUpdate = 1;
this.getDb();
SongScanner.service.submit(() -> {
Statement state;
ResultSet result;
Album album;
LocalSong song;
try
{
logger.debug("Querying database.");
state = this.getDb().createStatement();
result = state.executeQuery("SELECT * FROM LOCAL_ALBUMS;");
while (result.next())
{
album = new Album();
album.name = result.getString("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 * FROM LOCAL_SONGS;");
while (result.next())
{
song = new LocalSong();
song.file = new File(result.getString("file"));
song.codec = result.getString("codec");
song.type = result.getString("type");
song.title = result.getString("title");
song.artists = 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(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();
updatedSongs = 0;
totalUpdate = 0;
triggerUpdateListeners();
}
catch (SQLException e)
{
logger.error("Could not close database.", e);
}
}
});
}
/**
* Scans the local file system for song files.
*/
private void updateSongs()
{
SongScanner.service.submit(new SongScanner(source));
/*
* Resets the song scanner when ready.
*/
SongScanner.service.submit(() -> {
Statement stat;
ResultSet tableResult;
while (true)
{
try
if (SongScanner.service.awaitQuiescence(60, TimeUnit.SECONDS))
{
if (!SongScanner.service.awaitTermination(60, TimeUnit.SECONDS)) break;
}
catch (InterruptedException e)
{
logger.error("Error in waiting for song scan.", e);
break;
}
}
SongScanner.currentFolder = "";
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);");
}
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);
}
});
}

View File

@@ -10,6 +10,7 @@ import edu.regis.universeplayer.data.CollectionType;
import edu.regis.universeplayer.data.Song;
import edu.regis.universeplayer.data.SongProvider;
import edu.regis.universeplayer.data.UpdateListener;
import net.harawata.appdirs.AppDirsFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -19,6 +20,7 @@ import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
@@ -35,6 +37,9 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
{
private static final Logger logger = LoggerFactory.getLogger(Interface.class);
private static File dataDir;
private static File configDir;
/**
* A reference to the panel containing links to different collection views.
*/
@@ -123,6 +128,48 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
}
}
/**
* Obtains the data storage directory for the application, creating it if
* needed.
* @return The data storage directory.
*/
public static File getDataDir()
{
if (dataDir == null)
{
dataDir = new File(AppDirsFactory.getInstance().getUserDataDir("universalmusic", null, null, true));
if (!dataDir.exists())
{
if (!dataDir.mkdir())
{
logger.error("Could not create data directory {}", dataDir);
}
}
}
return dataDir;
}
/**
* Obtains the configuration directory for the application, creating it if
* needed.
* @return The configuration directory.
*/
public static File getConfigDir()
{
if (configDir == null)
{
configDir = new File(AppDirsFactory.getInstance().getUserConfigDir("universalmusic", null, null, true));
if (!configDir.exists())
{
if (!configDir.mkdir())
{
logger.error("Could not create configuration directory {}", configDir);
}
}
}
return configDir;
}
/**
* Creates an interface
*/

View File

@@ -17,7 +17,7 @@
<Root level="info">
<AppenderRef ref="File"/>
</Root>
<Logger name="edu.regis.universeplayer.player.Interface" level="debug">
<Logger name="edu.regis.universeplayer.data.LocalSongProvider" level="debug">
<AppenderRef ref="Console"/>
</Logger>
</Loggers>

View File

@@ -0,0 +1,65 @@
/*
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
*/
import edu.regis.universeplayer.player.Interface;
import org.junit.Test;
import java.io.File;
import java.sql.*;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue;
public class DBTest
{
@Test
public void test() throws ClassNotFoundException, SQLException
{
try
{
Class.forName("org.sqlite.JDBC");
Connection db;
Statement stat;
ResultSet tableResult;
// db = DriverManager.getConnection("jdbc:sqlite:" + new File(Interface.getDataDir().getAbsolutePath(), "universalmusictest.db").getAbsolutePath());
// db.setAutoCommit(false);
// stat = db.createStatement();
// tableResult = stat.executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='INFO';");
// if (!tableResult.next())
// {
// System.out.println("Creating song table");
// /*
// * Create the table
// */
// stat.executeUpdate("CREATE TABLE INFO" +
// "(DATA INTEGER PRIMARY KEY NOT NULL);");
// }
// else
// {
// System.out.println("Clearing table");
// stat.executeUpdate("DELETE FROM INFO;");
// }
// tableResult.close();
// stat.executeUpdate("INSERT INTO INFO (DATA) VALUES (1);");
// stat.close();
// db.commit();
// db.close();
db = DriverManager.getConnection("jdbc:sqlite:" + new File(Interface.getDataDir().getAbsolutePath(), "universalmusictest.db").getAbsolutePath());
db.setAutoCommit(false);
stat = db.createStatement();
tableResult = stat.executeQuery("SELECT (DATA) FROM INFO");
assertTrue(tableResult.next());
assertEquals(1, tableResult.getInt("DATA"));
tableResult.close();
stat.close();
db.close();
}
catch (SQLException e)
{
e.printStackTrace();
throw e;
}
}
}