Added library and indexing functionality for a music player.

This commit is contained in:
neviyn 2016-01-30 23:32:00 +00:00
commit dbe518faaa
11 changed files with 739 additions and 0 deletions

70
.gitignore vendored Normal file
View File

@ -0,0 +1,70 @@
### Java template
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.ear
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio
*.iml
## Directory-based project format:
.idea/
# if you remove the above rule, at least ignore the following:
# User-specific stuff:
# .idea/workspace.xml
# .idea/tasks.xml
# .idea/dictionaries
# Sensitive or high-churn files:
# .idea/dataSources.ids
# .idea/dataSources.xml
# .idea/sqlDataSources.xml
# .idea/dynamic.xml
# .idea/uiDesigner.xml
# Gradle:
# .idea/gradle.xml
# .idea/libraries
# Mongo Explorer plugin:
# .idea/mongoSettings.xml
## File-based project format:
*.ipr
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
### Maven template
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
# Created by .ignore support plugin (hsz.mobi)

42
pom.xml Normal file
View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>uk.co.neviyn</groupId>
<artifactId>musicplayer</artifactId>
<version>DEVELOPMENT</version>
<dependencies>
<dependency>
<groupId>net.jthink</groupId>
<artifactId>jaudiotagger</artifactId>
<version>2.2.3</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.0.7.Final</version>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>2.3.3</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>org.jboss.repository.releases</id>
<name>JBoss Maven Release Repository</name>
<url>https://repository.jboss.org/nexus/content/repositories/releases</url>
</repository>
<repository>
<id>jaudiotagger-repository</id>
<url>https://dl.bintray.com/ijabz/maven</url>
</repository>
</repositories>
</project>

View File

@ -0,0 +1,60 @@
package musicplayer;
import musicplayer.db.DatabaseManager;
import musicplayer.db.Gateway;
import musicplayer.model.ExtractedMetadata;
import org.jaudiotagger.audio.AudioFileIO;
import org.jaudiotagger.audio.exceptions.CannotReadException;
import org.jaudiotagger.audio.exceptions.InvalidAudioFrameException;
import org.jaudiotagger.audio.exceptions.ReadOnlyFileException;
import org.jaudiotagger.tag.Tag;
import org.jaudiotagger.tag.TagException;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
public class Application {
String musicFileExtensionRegex = ".*\\.(mp3|mp4|flac)";
public static void main(String[] args) {
new Application();
}
public Application(){
DatabaseManager.init();
}
/**
* Walk through all files and directories recursively and index any music files with a correct extension
* @param rootDirectory Directory from which to start searching
*/
public void processSongs(Path rootDirectory){
try {
Files.walk(rootDirectory)
.filter(f -> f.toString().matches(musicFileExtensionRegex))
.map(this::autoParse).filter(Optional::isPresent).map(Optional::get).forEach(Gateway::addSong);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Extract music metadata from the target file.
* @param targetFile Path to file to extract metadata from.
* @return Metadata contained in targetFile.
*/
public Optional<ExtractedMetadata> autoParse(Path targetFile){
Tag audioTags = null;
try {
audioTags = AudioFileIO.read(targetFile.toFile()).getTag();
} catch (CannotReadException | IOException | ReadOnlyFileException | TagException | InvalidAudioFrameException e) {
e.printStackTrace();
}
return audioTags == null ? Optional.empty() : Optional.of(new ExtractedMetadata(audioTags, targetFile.toFile()));
}
}

View File

@ -0,0 +1,73 @@
package musicplayer.db;
import musicplayer.model.Album;
import musicplayer.model.Artist;
import musicplayer.model.Song;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import java.util.Properties;
/**
* Singleton for managing connection to Hibernate.
*/
public class DatabaseManager {
private static DatabaseManager ourInstance = new DatabaseManager();
private SessionFactory sessionFactory;
public static DatabaseManager getInstance() {
return ourInstance;
}
private DatabaseManager() {
}
/**
* Open a SessionFactory to the database as defined in hibernate.cfg.xml
*/
public static void init(){
if(getInstance().sessionFactory != null)
getInstance().sessionFactory.close();
getInstance().sessionFactory = new Configuration().configure()
.addAnnotatedClass(Album.class)
.addAnnotatedClass(Artist.class)
.addAnnotatedClass(Song.class)
.buildSessionFactory();
}
/**
* Open a SessionFactory to an in-memory database for testing
*/
public static void testMode(){
if(getInstance().sessionFactory != null)
getInstance().sessionFactory.close();
Properties properties = new Properties();
properties.put("hibernate.dialect", "org.hibernate.dialect.HSQLDialect");
properties.put("hibernate.connection.driver_class", "org.hsqldb.jdbc.JDBCDriver");
properties.put("hibernate.connection.url", "jdbc:hsqldb:mem:.");
properties.put("hibernate.hbm2ddl.auto", "create-drop");
getInstance().sessionFactory = new Configuration()
.addProperties(properties)
.addAnnotatedClass(Album.class)
.addAnnotatedClass(Artist.class)
.addAnnotatedClass(Song.class)
.buildSessionFactory();
}
/**
* @return Session for database interaction.
*/
public Session getSession(){
return sessionFactory.openSession();
}
@SuppressWarnings("CloneDoesntCallSuperClone")
@Override
public Object clone() throws CloneNotSupportedException{
throw new CloneNotSupportedException();
}
}

View File

@ -0,0 +1,85 @@
package musicplayer.db;
import musicplayer.model.Album;
import musicplayer.model.Artist;
import musicplayer.model.ExtractedMetadata;
import musicplayer.model.Song;
import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.criterion.Restrictions;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Contains database queries.
*/
public class Gateway {
/**
* @param name Name of the album to find.
* @return Album found with name or Optional.empty()
*/
public static Optional<Album> getOneAlbum(String name){
try(Session session = DatabaseManager.getInstance().getSession()){
Criteria criteria = session.createCriteria(Album.class);
Album album = (Album)criteria.add(Restrictions.eq("name", name)).uniqueResult();
return (album == null) ? Optional.empty() : Optional.of(album);
}
}
/**
* @param name Name of the artist to find.
* @return Artist found with name or Optional.empty()
*/
public static Optional<Artist> getOneArtist(String name){
try(Session session = DatabaseManager.getInstance().getSession()){
Criteria criteria = session.createCriteria(Artist.class);
Artist artist = (Artist) criteria.add(Restrictions.eq("name", name)).uniqueResult();
return (artist == null) ? Optional.empty() : Optional.of(artist);
}
}
/**
* @return List of all songs currently stored in the database.
*/
public static Optional<List<Song>> listAllSongs(){
try(Session session = DatabaseManager.getInstance().getSession()){
@SuppressWarnings("unchecked")
List<Song> songs = session.createCriteria(Song.class).list();
return (songs == null || songs.isEmpty()) ? Optional.empty() : Optional.of(songs);
}
}
/**
* @return All songs currently in the database, grouped by album.
*/
public static Optional<Map<Album, List<Song>>> listAllSongsGroupedByAlbum(){
Optional<List<Song>> songList = listAllSongs();
return (songList.isPresent()) ?
Optional.of(songList.get().stream().collect(Collectors.groupingBy(Song::getAlbum)))
: Optional.empty();
}
/**
* Add a new song to the database.
* @param metadata New song information.
*/
public static void addSong(ExtractedMetadata metadata){
try(Session session = DatabaseManager.getInstance().getSession()) {
Optional<Album> albumObj = getOneAlbum(metadata.album);
if(!albumObj.isPresent())
albumObj = Optional.of(new Album(metadata.album));
Optional<Artist> artistObj = getOneArtist(metadata.artist);
if(!artistObj.isPresent())
artistObj = Optional.of(new Artist(metadata.artist));
session.beginTransaction();
session.save(new Song(metadata.trackNumber, metadata.title, artistObj.get(), albumObj.get(), metadata.genre, metadata.songFile));
session.getTransaction().commit();
}
}
}

View File

@ -0,0 +1,51 @@
package musicplayer.model;
import javax.persistence.*;
@Entity
public class Album {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
@Column(name = "id")
private long id;
@Column(name = "name")
private String name;
protected Album(){}
public Album(String name){
this.name = name;
}
public String toString(){
return String.format("ID: %d, Name: %s", id, name);
}
@SuppressWarnings("MethodWithMultipleReturnPoints")
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Album album = (Album) o;
return id == album.id && name.equals(album.name);
}
@Override
public int hashCode() {
int result = (int) (id ^ (id >>> 32));
result = 31 * result + name.hashCode();
return result;
}
public String getName(){
return name;
}
public long getId(){
return id;
}
}

View File

@ -0,0 +1,51 @@
package musicplayer.model;
import javax.persistence.*;
@Entity
public class Artist {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
@Column(name = "id")
private long id;
@Column(name = "name")
private String name;
protected Artist(){}
public Artist(String name){
this.name = name;
}
@Override
public int hashCode() {
int result = (int) (id ^ (id >>> 32));
result = 31 * result + name.hashCode();
return result;
}
public String toString(){
return String.format("ID: %d, Name: %s", id, name);
}
@SuppressWarnings("MethodWithMultipleReturnPoints")
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Artist artist = (Artist) o;
return id == artist.id && name.equals(artist.name);
}
public String getName(){
return name;
}
public long getId(){
return id;
}
}

View File

@ -0,0 +1,31 @@
package musicplayer.model;
import org.jaudiotagger.tag.FieldKey;
import org.jaudiotagger.tag.Tag;
import java.io.File;
/**
* Internal class representing metadata extracted from a music file.
*/
public class ExtractedMetadata {
public final String title;
public final String album;
public final String artist;
public final String genre;
public final String songFile;
public final String trackNumber;
/**
* @param audioTags jaudiotagger tag data.
* @param songFile Location of the song file on the filesystem.
*/
public ExtractedMetadata(Tag audioTags, File songFile){
this.trackNumber = audioTags.getFirst(FieldKey.TRACK);
this.title = audioTags.getFirst(FieldKey.TITLE);
this.album = audioTags.getFirst(FieldKey.ALBUM);
this.artist = audioTags.getFirst(FieldKey.ARTIST);
this.genre = audioTags.getFirst(FieldKey.GENRE);
this.songFile = songFile.getAbsolutePath();
}
}

View File

@ -0,0 +1,114 @@
package musicplayer.model;
import javax.imageio.ImageIO;
import javax.persistence.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
@Entity
public class Song {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
@Column(name = "id")
private long id;
@ManyToOne(fetch=FetchType.EAGER,cascade=CascadeType.ALL)
private Artist artist;
@Column(name = "genre")
private String genre;
@Column(name = "title")
private String title;
@ManyToOne(fetch=FetchType.EAGER,cascade=CascadeType.ALL)
private Album album;
@Column(name = "songFile")
private String songFile;
@Column(name = "trackNumber")
private String trackNumber;
protected Song(){}
public Song(String trackNumber, String title, Artist artist, Album album, String genre, String songFile){
this.trackNumber = trackNumber;
this.title = title;
this.artist = artist;
this.genre = genre;
this.album = album;
this.songFile = songFile;
}
@Override
public String toString(){
return String.format("Artist: %s, Title: %s, Album: %s, Genre: %s", artist.getName(), title, album.getName(), genre);
}
@SuppressWarnings("MethodWithMultipleReturnPoints")
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Song song = (Song) o;
return id == song.id && artist.equals(song.artist) && genre.equals(song.genre) && title.equals(song.title)
&& album.equals(song.album) && songFile.equals(song.songFile) && trackNumber.equals(song.trackNumber);
}
@Override
public int hashCode() {
int result = (int) (id ^ (id >>> 32));
result = 31 * result + artist.hashCode();
result = 31 * result + genre.hashCode();
result = 31 * result + title.hashCode();
result = 31 * result + album.hashCode();
result = 31 * result + songFile.hashCode();
result = 31 * result + trackNumber.hashCode();
return result;
}
/**
* Try to find album art for this song based on likely image file names in the folder.
* @return BufferedImage of album art or Optional.empty()
*/
public Optional<BufferedImage> getAlbumArt(){
try {
Optional<Path> imageFile = Files.walk(getSongFile().getParentFile().toPath())
.filter(f -> f.toString().matches("(Folder|Cover).jpg")).findFirst();
return (imageFile.isPresent()) ? Optional.of(ImageIO.read(imageFile.get().toFile())) : Optional.empty();
} catch (IOException e) {
return Optional.empty();
}
}
public long getId() {
return id;
}
public Artist getArtist() {
return artist;
}
public String getGenre() {
return genre;
}
public String getTitle() {
return title;
}
public Album getAlbum() {
return album;
}
public File getSongFile() {
return new File(songFile);
}
public String getTrackNumber() {
return trackNumber;
}
}

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-configuration SYSTEM "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<property name="hibernate.dialect">
org.hibernate.dialect.HSQLDialect
</property>
<property name="hibernate.connection.driver_class">
org.hsqldb.jdbc.JDBCDriver
</property>
<property name="hibernate.connection.url">
jdbc:hsqldb:file:E:/dev/testdb;shutdown=true
</property>
<property name="hibernate.connection.username">
root
</property>
<property name="hibernate.connection.password">
root123
</property>
<property name="hibernate.hbm2ddl.auto">
update
</property>
</session-factory>
</hibernate-configuration>

View File

@ -0,0 +1,138 @@
package musicplayer.db;
import musicplayer.model.Album;
import musicplayer.model.Artist;
import musicplayer.model.ExtractedMetadata;
import musicplayer.model.Song;
import org.hibernate.Session;
import org.jaudiotagger.tag.FieldKey;
import org.jaudiotagger.tag.Tag;
import org.jaudiotagger.tag.id3.ID3v1Tag;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.*;
public class GatewayTest {
private Session session;
@Before
public void setUp(){
DatabaseManager.testMode();
session = DatabaseManager.getInstance().getSession();
}
@After
public void tearDown(){
session.close();
session = null;
}
@Test
public void testGetOneAlbum() throws Exception {
String albumName = "Test Album";
Album testAlbum = new Album(albumName);
session.beginTransaction();
session.save(testAlbum);
session.getTransaction().commit();
assertTrue(Gateway.getOneAlbum(albumName).isPresent());
Album retrievedAlbum = Gateway.getOneAlbum(albumName).get();
assertEquals(testAlbum, retrievedAlbum);
}
@Test
public void testGetOneAlbumEmptyDB() throws Exception {
assertTrue(!Gateway.getOneAlbum("").isPresent());
}
@Test
public void testGetOneArtist() throws Exception {
String artistName = "Test Artist";
Artist testArtist = new Artist(artistName);
session.beginTransaction();
session.save(testArtist);
session.getTransaction().commit();
assertTrue(Gateway.getOneArtist(artistName).isPresent());
Artist retrievedArtist = Gateway.getOneArtist(artistName).get();
assertEquals(testArtist, retrievedArtist);
}
@Test
public void testGetOneArtistEmptyDB() throws Exception {
assertTrue(!Gateway.getOneArtist("").isPresent());
}
@Test
public void testListAllSongs() throws Exception {
Song song1 = new Song("1", "s1", new Artist("a"), new Album("a"), "", "");
Song song2 = new Song("2", "s2", new Artist("b"), new Album("a"), "", "");
Song song3 = new Song("1", "t1", new Artist("c"), new Album("b"), "", "");
session.beginTransaction();
session.save(song1);
session.save(song2);
session.save(song3);
session.getTransaction().commit();
assertTrue(Gateway.listAllSongs().isPresent());
List<Song> result = Gateway.listAllSongs().get();
assertTrue(result.size() == 3);
assertTrue(result.contains(song1));
assertTrue(result.contains(song2));
assertTrue(result.contains(song3));
}
@Test
public void testListAllSongsEmptyDB() throws Exception {
assertTrue(!Gateway.listAllSongs().isPresent());
}
@Test
public void testListAllSongsGroupedByAlbum() throws Exception {
Album album1 = new Album("Test 1");
Album album2 = new Album("Test 2");
Song song1 = new Song("1", "s1", new Artist("a"), album1, "", "");
Song song2 = new Song("2", "s2", new Artist("b"), album1, "", "");
Song song3 = new Song("1", "t1", new Artist("c"), album2, "", "");
session.beginTransaction();
session.save(song1);
session.save(song2);
session.save(song3);
session.getTransaction().commit();
assertTrue(Gateway.listAllSongsGroupedByAlbum().isPresent());
Map<Album, List<Song>> result = Gateway.listAllSongsGroupedByAlbum().get();
assertTrue(result.size() == 2);
assertTrue(result.get(album1).size() == 2);
assertTrue(result.get(album2).size() == 1);
assertTrue(result.get(album1).contains(song1));
assertTrue(result.get(album1).contains(song2));
assertTrue(result.get(album2).contains(song3));
}
@Test
public void testListAllSongsGroupedByAlbumEmptyDB() throws Exception {
assertTrue(!Gateway.listAllSongsGroupedByAlbum().isPresent());
}
@Test
public void testAddSong() throws Exception {
Tag tags = new ID3v1Tag();
tags.addField(FieldKey.ALBUM, "Test Album");
tags.addField(FieldKey.ARTIST, "Test Artist");
tags.addField(FieldKey.TRACK, "1");
tags.addField(FieldKey.TITLE, "Test Song");
tags.addField(FieldKey.GENRE, "Test Genre");
ExtractedMetadata metadata = new ExtractedMetadata(tags, new File(""));
Gateway.addSong(metadata);
Song song = (Song)session.createCriteria(Song.class).uniqueResult();
assertEquals(song.getAlbum().getName(), metadata.album);
assertEquals(song.getArtist().getName(), metadata.artist);
assertEquals(song.getTrackNumber(), metadata.trackNumber);
assertEquals(song.getTitle(), metadata.title);
assertEquals(song.getGenre(), metadata.genre);
}
}