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 'org.apache.logging.log4j:log4j-slf4j-impl:2.13.3'
|
||||||
|
|
||||||
implementation 'com.google.code.gson:gson:2.8.7'
|
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.googlecode.soundlibs:jlayer:1.0.1.4'
|
||||||
implementation 'com.mpatric:mp3agic:0.9.1'
|
implementation 'com.mpatric:mp3agic:0.9.1'
|
||||||
implementation project(":libwave")
|
implementation project(":libwave")
|
||||||
|
|||||||
@@ -4,15 +4,15 @@
|
|||||||
|
|
||||||
package edu.regis.universeplayer.data;
|
package edu.regis.universeplayer.data;
|
||||||
|
|
||||||
|
import edu.regis.universeplayer.player.Interface;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.sql.*;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.*;
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -24,6 +24,7 @@ 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 HashSet<LocalSong> songs = new HashSet<>();
|
private final HashSet<LocalSong> songs = new HashSet<>();
|
||||||
private final HashMap<String, Album> albums = new HashMap<>();
|
private final HashMap<String, Album> albums = new HashMap<>();
|
||||||
@@ -151,10 +152,9 @@ public class LocalSongProvider implements SongProvider<LocalSong>
|
|||||||
return codecs;
|
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 ForkJoinPool service = new ForkJoinPool();
|
||||||
private static final ExecutorService service = Executors.newFixedThreadPool(serviceThreads);
|
|
||||||
private static String currentFolder;
|
private static String currentFolder;
|
||||||
|
|
||||||
private final File file;
|
private final File file;
|
||||||
@@ -165,7 +165,7 @@ public class LocalSongProvider implements SongProvider<LocalSong>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run()
|
public void compute()
|
||||||
{
|
{
|
||||||
Process process;
|
Process process;
|
||||||
String line;
|
String line;
|
||||||
@@ -191,12 +191,11 @@ public class LocalSongProvider implements SongProvider<LocalSong>
|
|||||||
if (file.isDirectory())
|
if (file.isDirectory())
|
||||||
{
|
{
|
||||||
totalUpdate--;
|
totalUpdate--;
|
||||||
for (File subFile : Objects.requireNonNull(file.listFiles()))
|
List<SongScanner> tasks = Arrays.stream(Objects.requireNonNullElse(file.listFiles(), new File[0]))
|
||||||
{
|
.map(SongScanner::new).collect(Collectors.toList());
|
||||||
totalUpdate++;
|
totalUpdate += tasks.size();
|
||||||
triggerUpdateListeners();
|
triggerUpdateListeners();
|
||||||
service.submit(new SongScanner(subFile));
|
invokeAll(tasks);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (file.getName().lastIndexOf(".") < file.getName().length() - 1)
|
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)
|
public LocalSongProvider(File source)
|
||||||
{
|
{
|
||||||
|
Connection dbL = null;
|
||||||
this.source = source;
|
this.source = source;
|
||||||
if (this.source == null || !this.source.isDirectory())
|
if (this.source == null || !this.source.isDirectory())
|
||||||
{
|
{
|
||||||
throw new IllegalArgumentException("File source must be existing directory");
|
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;
|
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));
|
SongScanner.service.submit(new SongScanner(source));
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Resets the song scanner when ready.
|
* Resets the song scanner when ready.
|
||||||
*/
|
*/
|
||||||
SongScanner.service.submit(() -> {
|
SongScanner.service.submit(() -> {
|
||||||
|
Statement stat;
|
||||||
|
ResultSet tableResult;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
try
|
if (SongScanner.service.awaitQuiescence(60, TimeUnit.SECONDS))
|
||||||
{
|
{
|
||||||
if (!SongScanner.service.awaitTermination(60, TimeUnit.SECONDS)) break;
|
break;
|
||||||
}
|
|
||||||
catch (InterruptedException e)
|
|
||||||
{
|
|
||||||
logger.error("Error in waiting for song scan.", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SongScanner.currentFolder = "";
|
SongScanner.currentFolder = "";
|
||||||
updatedSongs = 0;
|
updatedSongs = 0;
|
||||||
totalUpdate = 0;
|
totalUpdate = 0;
|
||||||
triggerUpdateListeners();
|
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.Song;
|
||||||
import edu.regis.universeplayer.data.SongProvider;
|
import edu.regis.universeplayer.data.SongProvider;
|
||||||
import edu.regis.universeplayer.data.UpdateListener;
|
import edu.regis.universeplayer.data.UpdateListener;
|
||||||
|
import net.harawata.appdirs.AppDirsFactory;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ import java.awt.event.ComponentEvent;
|
|||||||
import java.awt.event.ComponentListener;
|
import java.awt.event.ComponentListener;
|
||||||
import java.awt.event.WindowEvent;
|
import java.awt.event.WindowEvent;
|
||||||
import java.awt.event.WindowListener;
|
import java.awt.event.WindowListener;
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
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 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.
|
* 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
|
* Creates an interface
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<Root level="info">
|
<Root level="info">
|
||||||
<AppenderRef ref="File"/>
|
<AppenderRef ref="File"/>
|
||||||
</Root>
|
</Root>
|
||||||
<Logger name="edu.regis.universeplayer.player.Interface" level="debug">
|
<Logger name="edu.regis.universeplayer.data.LocalSongProvider" level="debug">
|
||||||
<AppenderRef ref="Console"/>
|
<AppenderRef ref="Console"/>
|
||||||
</Logger>
|
</Logger>
|
||||||
</Loggers>
|
</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