Added album art icons and caching.

This commit is contained in:
neviyn 2016-02-10 19:31:00 +00:00
parent a1515ee9ad
commit 1d860760d6
10 changed files with 211 additions and 75 deletions

View File

@ -11,8 +11,9 @@ import org.jaudiotagger.audio.exceptions.ReadOnlyFileException;
import org.jaudiotagger.tag.Tag; import org.jaudiotagger.tag.Tag;
import org.jaudiotagger.tag.TagException; import org.jaudiotagger.tag.TagException;
import java.io.*; import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List; import java.util.List;
@ -21,15 +22,15 @@ import java.util.Optional;
public class Application { public class Application {
final String musicFileExtensionRegex = ".*\\.(mp3|mp4|flac)"; static final String musicFileExtensionRegex = ".*\\.(mp3|mp4|flac)";
public Application(){ public Application() {
DatabaseManager.init(); DatabaseManager.init();
List<Song> songs = Gateway.listAllSongs().get(); List<Song> songs = Gateway.listAllSongs().get();
boolean running = true; boolean running = true;
BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
Player player = new Player(); Player player = new Player();
while(running){ while (running) {
try { try {
System.out.println("Ready to read"); System.out.println("Ready to read");
String input = br.readLine(); String input = br.readLine();
@ -58,13 +59,14 @@ public class Application {
/** /**
* Walk through all files and directories recursively and index any music files with a correct extension * Walk through all files and directories recursively and index any music files with a correct extension
*
* @param rootDirectory Directory from which to start searching * @param rootDirectory Directory from which to start searching
*/ */
public void processSongs(Path rootDirectory){ public static void processSongs(Path rootDirectory) {
try { try {
Files.walk(rootDirectory) Files.walk(rootDirectory)
.filter(f -> f.toString().matches(musicFileExtensionRegex)) .filter(f -> f.toString().matches(musicFileExtensionRegex))
.map(this::autoParse).filter(Optional::isPresent).map(Optional::get).forEach(Gateway::addSong); .map(Application::autoParse).filter(Optional::isPresent).map(Optional::get).forEach(Gateway::addSong);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} }
@ -72,10 +74,11 @@ public class Application {
/** /**
* Extract music metadata from the target file. * Extract music metadata from the target file.
*
* @param targetFile Path to file to extract metadata from. * @param targetFile Path to file to extract metadata from.
* @return Metadata contained in targetFile. * @return Metadata contained in targetFile.
*/ */
public Optional<ExtractedMetadata> autoParse(Path targetFile){ public static Optional<ExtractedMetadata> autoParse(Path targetFile) {
Tag audioTags = null; Tag audioTags = null;
try { try {
audioTags = AudioFileIO.read(targetFile.toFile()).getTag(); audioTags = AudioFileIO.read(targetFile.toFile()).getTag();

View File

@ -0,0 +1,34 @@
package musicplayer;
import musicplayer.model.Album;
import javax.swing.*;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeCellRenderer;
import java.awt.*;
public class LibraryTreeCellRenderer implements TreeCellRenderer {
private JLabel label;
LibraryTreeCellRenderer() {
label = new JLabel();
}
@Override
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
Object o = ((DefaultMutableTreeNode) value).getUserObject();
if (o instanceof Album) {
Album album = (Album) o;
if (album.getAlbumArt().isPresent())
label.setIcon(new ImageIcon(album.getAlbumArt().get()));
else {
label.setIcon(null);
}
} else
label.setIcon(null);
if (o != null)
label.setText(o.toString());
return label;
}
}

View File

@ -7,21 +7,21 @@ import org.gstreamer.elements.PlayBin2;
import java.io.File; import java.io.File;
public class Player{ public class Player {
private PlayBin2 playBin; private PlayBin2 playBin;
private InternalThread internalThread; private InternalThread internalThread;
private Thread thread; private Thread thread;
public Player(){ public Player() {
Gst.init(); Gst.init();
playBin = new PlayBin2("BusMessages"); playBin = new PlayBin2("BusMessages");
playBin.setVideoSink(ElementFactory.make("fakesink", "videosink")); playBin.setVideoSink(ElementFactory.make("fakesink", "videosink"));
} }
public void playSong(File songFile){ public void playSong(File songFile) {
if(playBin.getState() == State.PLAYING) if (playBin.getState() == State.PLAYING)
stop(); stop();
playBin.setURI(songFile.toURI()); playBin.setURI(songFile.toURI());
internalThread = new InternalThread(); internalThread = new InternalThread();
@ -39,11 +39,11 @@ public class Player{
} }
} }
public void resume(){ public void resume() {
playBin.play(); playBin.play();
} }
public void pause(){ public void pause() {
playBin.pause(); playBin.pause();
} }

View File

@ -11,6 +11,8 @@ import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeNode; import javax.swing.tree.TreeNode;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.TreeMap;
import java.util.TreeSet;
public class PlayerGUI { public class PlayerGUI {
private JTree libraryView; private JTree libraryView;
@ -27,42 +29,43 @@ public class PlayerGUI {
} }
public PlayerGUI(){ public PlayerGUI() {
DatabaseManager.init(); DatabaseManager.init();
populateLibrary(Gateway.listAllSongsGroupedByAlbum().get()); populateLibrary(new TreeMap<>(Gateway.listAllSongsGroupedByAlbum().get()));
} }
private void resetTree(){ private void resetTree() {
libraryView.removeAll(); libraryView.removeAll();
DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(); DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode();
DefaultTreeModel treeModel = new DefaultTreeModel(rootNode); DefaultTreeModel treeModel = new DefaultTreeModel(rootNode);
libraryView.setModel(treeModel); libraryView.setModel(treeModel);
libraryView.setRootVisible(false); libraryView.setRootVisible(false);
libraryView.setToggleClickCount(1); libraryView.setToggleClickCount(1);
libraryView.setCellRenderer(new LibraryTreeCellRenderer());
} }
private void populateLibrary(Map<Album, List<Song>> libraryData){ private void populateLibrary(Map<Album, List<Song>> libraryData) {
resetTree(); resetTree();
DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode)libraryView.getModel().getRoot(); DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) libraryView.getModel().getRoot();
libraryData.forEach((k, v) -> { libraryData.forEach((k, v) -> {
DefaultMutableTreeNode albumNode = new DefaultMutableTreeNode(k); DefaultMutableTreeNode albumNode = new DefaultMutableTreeNode(k);
addNodeToTreeModel(parentNode, albumNode); addNodeToTreeModel(parentNode, albumNode);
v.forEach(x -> addNodeToTreeModel(albumNode, new DefaultMutableTreeNode(x))); new TreeSet<>(v).forEach(x -> addNodeToTreeModel(albumNode, new DefaultMutableTreeNode(x)));
}); });
} }
private void populateLibrary(List<Song> libraryData){ private void populateLibrary(List<Song> libraryData) {
resetTree(); resetTree();
DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode)libraryView.getModel().getRoot(); DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) libraryView.getModel().getRoot();
libraryData.forEach(x -> addNodeToTreeModel(parentNode, new DefaultMutableTreeNode(x))); libraryData.forEach(x -> addNodeToTreeModel(parentNode, new DefaultMutableTreeNode(x)));
} }
private void addNodeToTreeModel(DefaultMutableTreeNode parentNode, DefaultMutableTreeNode node ) { private void addNodeToTreeModel(DefaultMutableTreeNode parentNode, DefaultMutableTreeNode node) {
DefaultTreeModel libraryModel = (DefaultTreeModel)libraryView.getModel(); DefaultTreeModel libraryModel = (DefaultTreeModel) libraryView.getModel();
libraryModel.insertNodeInto(node, parentNode, parentNode.getChildCount()); libraryModel.insertNodeInto(node, parentNode, parentNode.getChildCount());
if (parentNode == libraryModel.getRoot()) { if (parentNode == libraryModel.getRoot()) {
libraryModel.nodeStructureChanged((TreeNode)libraryModel.getRoot()); libraryModel.nodeStructureChanged((TreeNode) libraryModel.getRoot());
} }
} }

View File

@ -28,8 +28,8 @@ public class DatabaseManager {
/** /**
* Open a SessionFactory to the database as defined in hibernate.cfg.xml * Open a SessionFactory to the database as defined in hibernate.cfg.xml
*/ */
public static void init(){ public static void init() {
if(getInstance().sessionFactory != null) if (getInstance().sessionFactory != null)
getInstance().sessionFactory.close(); getInstance().sessionFactory.close();
getInstance().sessionFactory = new Configuration().configure() getInstance().sessionFactory = new Configuration().configure()
.addAnnotatedClass(Album.class) .addAnnotatedClass(Album.class)
@ -41,8 +41,8 @@ public class DatabaseManager {
/** /**
* Open a SessionFactory to an in-memory database for testing * Open a SessionFactory to an in-memory database for testing
*/ */
public static void testMode(){ public static void testMode() {
if(getInstance().sessionFactory != null) if (getInstance().sessionFactory != null)
getInstance().sessionFactory.close(); getInstance().sessionFactory.close();
Properties properties = new Properties(); Properties properties = new Properties();
properties.put("hibernate.dialect", "org.hibernate.dialect.HSQLDialect"); properties.put("hibernate.dialect", "org.hibernate.dialect.HSQLDialect");
@ -61,13 +61,13 @@ public class DatabaseManager {
/** /**
* @return Session for database interaction. * @return Session for database interaction.
*/ */
public Session getSession(){ public Session getSession() {
return sessionFactory.openSession(); return sessionFactory.openSession();
} }
@SuppressWarnings("CloneDoesntCallSuperClone") @SuppressWarnings("CloneDoesntCallSuperClone")
@Override @Override
public Object clone() throws CloneNotSupportedException{ public Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException(); throw new CloneNotSupportedException();
} }
} }

View File

@ -22,10 +22,10 @@ public class Gateway {
* @param name Name of the album to find. * @param name Name of the album to find.
* @return Album found with name or Optional.empty() * @return Album found with name or Optional.empty()
*/ */
public static Optional<Album> getOneAlbum(String name){ public static Optional<Album> getOneAlbum(String name) {
try(Session session = DatabaseManager.getInstance().getSession()){ try (Session session = DatabaseManager.getInstance().getSession()) {
Criteria criteria = session.createCriteria(Album.class); Criteria criteria = session.createCriteria(Album.class);
Album album = (Album)criteria.add(Restrictions.eq("name", name)).uniqueResult(); Album album = (Album) criteria.add(Restrictions.eq("name", name)).uniqueResult();
return (album == null) ? Optional.empty() : Optional.of(album); return (album == null) ? Optional.empty() : Optional.of(album);
} }
} }
@ -34,8 +34,8 @@ public class Gateway {
* @param name Name of the artist to find. * @param name Name of the artist to find.
* @return Artist found with name or Optional.empty() * @return Artist found with name or Optional.empty()
*/ */
public static Optional<Artist> getOneArtist(String name){ public static Optional<Artist> getOneArtist(String name) {
try(Session session = DatabaseManager.getInstance().getSession()){ try (Session session = DatabaseManager.getInstance().getSession()) {
Criteria criteria = session.createCriteria(Artist.class); Criteria criteria = session.createCriteria(Artist.class);
Artist artist = (Artist) criteria.add(Restrictions.eq("name", name)).uniqueResult(); Artist artist = (Artist) criteria.add(Restrictions.eq("name", name)).uniqueResult();
return (artist == null) ? Optional.empty() : Optional.of(artist); return (artist == null) ? Optional.empty() : Optional.of(artist);
@ -45,8 +45,8 @@ public class Gateway {
/** /**
* @return List of all songs currently stored in the database. * @return List of all songs currently stored in the database.
*/ */
public static Optional<List<Song>> listAllSongs(){ public static Optional<List<Song>> listAllSongs() {
try(Session session = DatabaseManager.getInstance().getSession()){ try (Session session = DatabaseManager.getInstance().getSession()) {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
List<Song> songs = session.createCriteria(Song.class).list(); List<Song> songs = session.createCriteria(Song.class).list();
return (songs == null || songs.isEmpty()) ? Optional.empty() : Optional.of(songs); return (songs == null || songs.isEmpty()) ? Optional.empty() : Optional.of(songs);
@ -56,7 +56,7 @@ public class Gateway {
/** /**
* @return All songs currently in the database, grouped by album. * @return All songs currently in the database, grouped by album.
*/ */
public static Optional<Map<Album, List<Song>>> listAllSongsGroupedByAlbum(){ public static Optional<Map<Album, List<Song>>> listAllSongsGroupedByAlbum() {
Optional<List<Song>> songList = listAllSongs(); Optional<List<Song>> songList = listAllSongs();
return (songList.isPresent()) ? return (songList.isPresent()) ?
Optional.of(songList.get().stream().collect(Collectors.groupingBy(Song::getAlbum))) Optional.of(songList.get().stream().collect(Collectors.groupingBy(Song::getAlbum)))
@ -65,21 +65,38 @@ public class Gateway {
/** /**
* Add a new song to the database. * Add a new song to the database.
*
* @param metadata New song information. * @param metadata New song information.
*/ */
public static void addSong(ExtractedMetadata metadata){ public static void addSong(ExtractedMetadata metadata) {
try(Session session = DatabaseManager.getInstance().getSession()) { 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.beginTransaction();
Optional<Album> albumObj = getOneAlbum(metadata.album);
if (!albumObj.isPresent()) {
Album album = new Album(metadata.album);
albumObj = Optional.of(album);
session.save(album);
}
Optional<Artist> artistObj = getOneArtist(metadata.artist);
if (!artistObj.isPresent()) {
Artist artist = new Artist(metadata.artist);
artistObj = Optional.of(artist);
session.save(artist);
}
session.save(new Song(metadata.trackNumber, metadata.title, artistObj.get(), albumObj.get(), metadata.genre, metadata.songFile)); session.save(new Song(metadata.trackNumber, metadata.title, artistObj.get(), albumObj.get(), metadata.genre, metadata.songFile));
session.getTransaction().commit(); session.getTransaction().commit();
} }
} }
public static void updateAllAlbumArt() {
try (Session session = DatabaseManager.getInstance().getSession()) {
@SuppressWarnings("unchecked")
List<Album> albums = session.createCriteria(Album.class).list();
session.beginTransaction();
albums.forEach(Album::refreshAlbumArt);
session.getTransaction().commit();
}
}
} }

View File

@ -1,27 +1,71 @@
package musicplayer.model; package musicplayer.model;
import javax.imageio.ImageIO;
import javax.persistence.*; import javax.persistence.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Optional;
import java.util.Set;
@Entity @Entity
public class Album { public class Album implements Comparable<Album> {
@Id @Id
@GeneratedValue(strategy=GenerationType.AUTO) @GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id") @Column(name = "id")
private long id; private long id;
@Column(name = "name") @Column(name = "name")
private String name; private String name;
@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "album")
private Set<Song> songs;
@Lob
@Column(name = "art", nullable = true, length = 10000)
private byte[] art;
@Transient
private static int imageScaleToSize = 50;
protected Album(){} protected Album() {
}
public Album(String name){ public Album(String name) {
this.name = name; this.name = name;
} }
public String toString(){ public String toString() {
return name; return name;
} }
public Set<Song> getSongs() {
return songs;
}
public Optional<Image> getAlbumArt() {
if (art == null) return Optional.empty();
try (InputStream in = new ByteArrayInputStream(art)) {
return Optional.of(ImageIO.read(in));
} catch (IOException e) {
return Optional.empty();
}
}
@Transient
public void refreshAlbumArt() {
Optional<BufferedImage> artGet = songs.iterator().next().getAlbumArt();
if (artGet.isPresent()) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
ImageIO.write(convertToBufferedImage(artGet.get().getScaledInstance(imageScaleToSize, imageScaleToSize, Image.SCALE_SMOOTH)), "jpg", baos);
baos.flush();
art = baos.toByteArray();
} catch (IOException e) {
art = null;
}
}
}
@SuppressWarnings("MethodWithMultipleReturnPoints") @SuppressWarnings("MethodWithMultipleReturnPoints")
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
@ -41,11 +85,27 @@ public class Album {
return result; return result;
} }
public String getName(){ @Transient
public static BufferedImage convertToBufferedImage(Image image) {
BufferedImage newImage = new BufferedImage(
image.getWidth(null), image.getHeight(null),
BufferedImage.TYPE_INT_ARGB);
Graphics2D g = newImage.createGraphics();
g.drawImage(image, 0, 0, null);
g.dispose();
return newImage;
}
public String getName() {
return name; return name;
} }
public long getId(){ public long getId() {
return id; return id;
} }
@Override
public int compareTo(Album o) {
return toString().compareToIgnoreCase(o.toString());
}
} }

View File

@ -1,20 +1,24 @@
package musicplayer.model; package musicplayer.model;
import javax.persistence.*; import javax.persistence.*;
import java.util.Set;
@Entity @Entity
public class Artist { public class Artist {
@Id @Id
@GeneratedValue(strategy=GenerationType.AUTO) @GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id") @Column(name = "id")
private long id; private long id;
@Column(name = "name") @Column(name = "name")
private String name; private String name;
@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "artist")
private Set<Song> songs;
protected Artist(){} protected Artist() {
}
public Artist(String name){ public Artist(String name) {
this.name = name; this.name = name;
} }
@ -25,10 +29,14 @@ public class Artist {
return result; return result;
} }
public String toString(){ public String toString() {
return name; return name;
} }
public Set<Song> getSongs() {
return songs;
}
@SuppressWarnings("MethodWithMultipleReturnPoints") @SuppressWarnings("MethodWithMultipleReturnPoints")
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
@ -41,11 +49,11 @@ public class Artist {
} }
public String getName(){ public String getName() {
return name; return name;
} }
public long getId(){ public long getId() {
return id; return id;
} }
} }

View File

@ -20,7 +20,7 @@ public class ExtractedMetadata {
* @param audioTags jaudiotagger tag data. * @param audioTags jaudiotagger tag data.
* @param songFile Location of the song file on the filesystem. * @param songFile Location of the song file on the filesystem.
*/ */
public ExtractedMetadata(Tag audioTags, File songFile){ public ExtractedMetadata(Tag audioTags, File songFile) {
this.trackNumber = audioTags.getFirst(FieldKey.TRACK); this.trackNumber = audioTags.getFirst(FieldKey.TRACK);
this.title = audioTags.getFirst(FieldKey.TITLE); this.title = audioTags.getFirst(FieldKey.TITLE);
this.album = audioTags.getFirst(FieldKey.ALBUM); this.album = audioTags.getFirst(FieldKey.ALBUM);

View File

@ -5,33 +5,37 @@ import javax.persistence.*;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.NoSuchElementException;
import java.util.Optional; import java.util.Optional;
@Entity @Entity
public class Song { public class Song implements Comparable<Song> {
@Id @Id
@GeneratedValue(strategy=GenerationType.AUTO) @GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id") @Column(name = "id")
private long id; private long id;
@ManyToOne(fetch=FetchType.EAGER,cascade=CascadeType.ALL) @ManyToOne
private Artist artist; private Artist artist;
@Column(name = "genre") @Column(name = "genre")
private String genre; private String genre;
@Column(name = "title") @Column(name = "title")
private String title; private String title;
@ManyToOne(fetch=FetchType.EAGER,cascade=CascadeType.ALL) @ManyToOne
private Album album; private Album album;
@Column(name = "songFile") @Column(name = "songFile")
private String songFile; private String songFile;
@Column(name = "trackNumber") @Column(name = "trackNumber")
private String trackNumber; private String trackNumber;
protected Song(){} protected Song() {
}
public Song(String trackNumber, String title, Artist artist, Album album, String genre, String songFile){ public Song(String trackNumber, String title, Artist artist, Album album, String genre, String songFile) {
this.trackNumber = trackNumber; this.trackNumber = trackNumber;
this.title = title; this.title = title;
this.artist = artist; this.artist = artist;
@ -41,7 +45,7 @@ public class Song {
} }
@Override @Override
public String toString(){ public String toString() {
return title; return title;
} }
@ -72,14 +76,14 @@ public class Song {
/** /**
* Try to find album art for this song based on likely image file names in the folder. * 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() * @return BufferedImage of album art or Optional.empty()
*/ */
public Optional<BufferedImage> getAlbumArt(){ public Optional<BufferedImage> getAlbumArt() {
try { Path dir = Paths.get(songFile).getParent();
Optional<Path> imageFile = Files.walk(getSongFile().getParentFile().toPath()) try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.{jpg,png}")) {
.filter(f -> f.toString().matches("(Folder|Cover).jpg")).findFirst(); return Optional.of(ImageIO.read(stream.iterator().next().toFile()));
return (imageFile.isPresent()) ? Optional.of(ImageIO.read(imageFile.get().toFile())) : Optional.empty(); } catch (IOException | NoSuchElementException ignored) {
} catch (IOException e) {
return Optional.empty(); return Optional.empty();
} }
} }
@ -111,4 +115,11 @@ public class Song {
public String getTrackNumber() { public String getTrackNumber() {
return trackNumber; return trackNumber;
} }
@Override
public int compareTo(Song o) {
if (trackNumber != null && o.getTrackNumber() != null && !trackNumber.isEmpty() && !o.getTrackNumber().isEmpty())
return Integer.parseInt(getTrackNumber()) - Integer.parseInt(o.getTrackNumber());
return (int) (id - o.getId());
}
} }