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:
@@ -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")
|
||||
|
||||
@@ -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,10 +152,9 @@ 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;
|
||||
@@ -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++;
|
||||
List<SongScanner> tasks = Arrays.stream(Objects.requireNonNullElse(file.listFiles(), new File[0]))
|
||||
.map(SongScanner::new).collect(Collectors.toList());
|
||||
totalUpdate += tasks.size();
|
||||
triggerUpdateListeners();
|
||||
service.submit(new SongScanner(subFile));
|
||||
}
|
||||
invokeAll(tasks);
|
||||
}
|
||||
else if (file.getName().lastIndexOf(".") < file.getName().length() - 1)
|
||||
{
|
||||
@@ -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
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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>
|
||||
|
||||
65
interface/src/test/java/DBTest.java
Normal file
65
interface/src/test/java/DBTest.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user