From 1d860760d6efe1f4c280df657f726ccda68763d6 Mon Sep 17 00:00:00 2001 From: Nathan Cannon Date: Wed, 10 Feb 2016 19:31:00 +0000 Subject: [PATCH] Added album art icons and caching. --- src/main/java/musicplayer/Application.java | 19 +++-- .../musicplayer/LibraryTreeCellRenderer.java | 34 +++++++++ src/main/java/musicplayer/Player.java | 12 +-- src/main/java/musicplayer/PlayerGUI.java | 25 ++++--- .../java/musicplayer/db/DatabaseManager.java | 12 +-- src/main/java/musicplayer/db/Gateway.java | 49 ++++++++---- src/main/java/musicplayer/model/Album.java | 74 +++++++++++++++++-- src/main/java/musicplayer/model/Artist.java | 20 +++-- .../musicplayer/model/ExtractedMetadata.java | 4 +- src/main/java/musicplayer/model/Song.java | 37 ++++++---- 10 files changed, 211 insertions(+), 75 deletions(-) create mode 100644 src/main/java/musicplayer/LibraryTreeCellRenderer.java diff --git a/src/main/java/musicplayer/Application.java b/src/main/java/musicplayer/Application.java index d924142..cac2211 100644 --- a/src/main/java/musicplayer/Application.java +++ b/src/main/java/musicplayer/Application.java @@ -11,8 +11,9 @@ import org.jaudiotagger.audio.exceptions.ReadOnlyFileException; import org.jaudiotagger.tag.Tag; 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.Path; import java.util.List; @@ -21,15 +22,15 @@ import java.util.Optional; public class Application { - final String musicFileExtensionRegex = ".*\\.(mp3|mp4|flac)"; + static final String musicFileExtensionRegex = ".*\\.(mp3|mp4|flac)"; - public Application(){ + public Application() { DatabaseManager.init(); List songs = Gateway.listAllSongs().get(); boolean running = true; BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); Player player = new Player(); - while(running){ + while (running) { try { System.out.println("Ready to read"); 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 + * * @param rootDirectory Directory from which to start searching */ - public void processSongs(Path rootDirectory){ + public static 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); + .map(Application::autoParse).filter(Optional::isPresent).map(Optional::get).forEach(Gateway::addSong); } catch (IOException e) { e.printStackTrace(); } @@ -72,10 +74,11 @@ public class Application { /** * Extract music metadata from the target file. + * * @param targetFile Path to file to extract metadata from. * @return Metadata contained in targetFile. */ - public Optional autoParse(Path targetFile){ + public static Optional autoParse(Path targetFile) { Tag audioTags = null; try { audioTags = AudioFileIO.read(targetFile.toFile()).getTag(); diff --git a/src/main/java/musicplayer/LibraryTreeCellRenderer.java b/src/main/java/musicplayer/LibraryTreeCellRenderer.java new file mode 100644 index 0000000..74310d4 --- /dev/null +++ b/src/main/java/musicplayer/LibraryTreeCellRenderer.java @@ -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; + } +} diff --git a/src/main/java/musicplayer/Player.java b/src/main/java/musicplayer/Player.java index cfec8c8..3c0b9e2 100644 --- a/src/main/java/musicplayer/Player.java +++ b/src/main/java/musicplayer/Player.java @@ -7,21 +7,21 @@ import org.gstreamer.elements.PlayBin2; import java.io.File; -public class Player{ +public class Player { private PlayBin2 playBin; private InternalThread internalThread; private Thread thread; - public Player(){ + public Player() { Gst.init(); playBin = new PlayBin2("BusMessages"); playBin.setVideoSink(ElementFactory.make("fakesink", "videosink")); } - public void playSong(File songFile){ - if(playBin.getState() == State.PLAYING) + public void playSong(File songFile) { + if (playBin.getState() == State.PLAYING) stop(); playBin.setURI(songFile.toURI()); internalThread = new InternalThread(); @@ -39,11 +39,11 @@ public class Player{ } } - public void resume(){ + public void resume() { playBin.play(); } - public void pause(){ + public void pause() { playBin.pause(); } diff --git a/src/main/java/musicplayer/PlayerGUI.java b/src/main/java/musicplayer/PlayerGUI.java index 7c675de..d507ddb 100644 --- a/src/main/java/musicplayer/PlayerGUI.java +++ b/src/main/java/musicplayer/PlayerGUI.java @@ -11,6 +11,8 @@ import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreeNode; import java.util.List; import java.util.Map; +import java.util.TreeMap; +import java.util.TreeSet; public class PlayerGUI { private JTree libraryView; @@ -27,42 +29,43 @@ public class PlayerGUI { } - public PlayerGUI(){ + public PlayerGUI() { DatabaseManager.init(); - populateLibrary(Gateway.listAllSongsGroupedByAlbum().get()); + populateLibrary(new TreeMap<>(Gateway.listAllSongsGroupedByAlbum().get())); } - private void resetTree(){ + private void resetTree() { libraryView.removeAll(); DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(); DefaultTreeModel treeModel = new DefaultTreeModel(rootNode); libraryView.setModel(treeModel); libraryView.setRootVisible(false); libraryView.setToggleClickCount(1); + libraryView.setCellRenderer(new LibraryTreeCellRenderer()); } - private void populateLibrary(Map> libraryData){ + private void populateLibrary(Map> libraryData) { resetTree(); - DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode)libraryView.getModel().getRoot(); + DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) libraryView.getModel().getRoot(); libraryData.forEach((k, v) -> { DefaultMutableTreeNode albumNode = new DefaultMutableTreeNode(k); 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 libraryData){ + private void populateLibrary(List libraryData) { resetTree(); - DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode)libraryView.getModel().getRoot(); + DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) libraryView.getModel().getRoot(); libraryData.forEach(x -> addNodeToTreeModel(parentNode, new DefaultMutableTreeNode(x))); } - private void addNodeToTreeModel(DefaultMutableTreeNode parentNode, DefaultMutableTreeNode node ) { - DefaultTreeModel libraryModel = (DefaultTreeModel)libraryView.getModel(); + private void addNodeToTreeModel(DefaultMutableTreeNode parentNode, DefaultMutableTreeNode node) { + DefaultTreeModel libraryModel = (DefaultTreeModel) libraryView.getModel(); libraryModel.insertNodeInto(node, parentNode, parentNode.getChildCount()); if (parentNode == libraryModel.getRoot()) { - libraryModel.nodeStructureChanged((TreeNode)libraryModel.getRoot()); + libraryModel.nodeStructureChanged((TreeNode) libraryModel.getRoot()); } } diff --git a/src/main/java/musicplayer/db/DatabaseManager.java b/src/main/java/musicplayer/db/DatabaseManager.java index 205756b..3bfd7e4 100644 --- a/src/main/java/musicplayer/db/DatabaseManager.java +++ b/src/main/java/musicplayer/db/DatabaseManager.java @@ -28,8 +28,8 @@ public class DatabaseManager { /** * Open a SessionFactory to the database as defined in hibernate.cfg.xml */ - public static void init(){ - if(getInstance().sessionFactory != null) + public static void init() { + if (getInstance().sessionFactory != null) getInstance().sessionFactory.close(); getInstance().sessionFactory = new Configuration().configure() .addAnnotatedClass(Album.class) @@ -41,8 +41,8 @@ public class DatabaseManager { /** * Open a SessionFactory to an in-memory database for testing */ - public static void testMode(){ - if(getInstance().sessionFactory != null) + public static void testMode() { + if (getInstance().sessionFactory != null) getInstance().sessionFactory.close(); Properties properties = new Properties(); properties.put("hibernate.dialect", "org.hibernate.dialect.HSQLDialect"); @@ -61,13 +61,13 @@ public class DatabaseManager { /** * @return Session for database interaction. */ - public Session getSession(){ + public Session getSession() { return sessionFactory.openSession(); } @SuppressWarnings("CloneDoesntCallSuperClone") @Override - public Object clone() throws CloneNotSupportedException{ + public Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException(); } } diff --git a/src/main/java/musicplayer/db/Gateway.java b/src/main/java/musicplayer/db/Gateway.java index d4022e8..a5fc84c 100644 --- a/src/main/java/musicplayer/db/Gateway.java +++ b/src/main/java/musicplayer/db/Gateway.java @@ -22,10 +22,10 @@ public class Gateway { * @param name Name of the album to find. * @return Album found with name or Optional.empty() */ - public static Optional getOneAlbum(String name){ - try(Session session = DatabaseManager.getInstance().getSession()){ + public static Optional 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(); + Album album = (Album) criteria.add(Restrictions.eq("name", name)).uniqueResult(); return (album == null) ? Optional.empty() : Optional.of(album); } } @@ -34,8 +34,8 @@ public class Gateway { * @param name Name of the artist to find. * @return Artist found with name or Optional.empty() */ - public static Optional getOneArtist(String name){ - try(Session session = DatabaseManager.getInstance().getSession()){ + public static Optional 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); @@ -45,8 +45,8 @@ public class Gateway { /** * @return List of all songs currently stored in the database. */ - public static Optional> listAllSongs(){ - try(Session session = DatabaseManager.getInstance().getSession()){ + public static Optional> listAllSongs() { + try (Session session = DatabaseManager.getInstance().getSession()) { @SuppressWarnings("unchecked") List songs = session.createCriteria(Song.class).list(); 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. */ - public static Optional>> listAllSongsGroupedByAlbum(){ + public static Optional>> listAllSongsGroupedByAlbum() { Optional> songList = listAllSongs(); return (songList.isPresent()) ? Optional.of(songList.get().stream().collect(Collectors.groupingBy(Song::getAlbum))) @@ -65,21 +65,38 @@ public class Gateway { /** * 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 albumObj = getOneAlbum(metadata.album); - if(!albumObj.isPresent()) - albumObj = Optional.of(new Album(metadata.album)); - Optional artistObj = getOneArtist(metadata.artist); - if(!artistObj.isPresent()) - artistObj = Optional.of(new Artist(metadata.artist)); + public static void addSong(ExtractedMetadata metadata) { + try (Session session = DatabaseManager.getInstance().getSession()) { session.beginTransaction(); + Optional albumObj = getOneAlbum(metadata.album); + if (!albumObj.isPresent()) { + Album album = new Album(metadata.album); + albumObj = Optional.of(album); + session.save(album); + } + Optional 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.getTransaction().commit(); } } + public static void updateAllAlbumArt() { + try (Session session = DatabaseManager.getInstance().getSession()) { + @SuppressWarnings("unchecked") + List albums = session.createCriteria(Album.class).list(); + session.beginTransaction(); + albums.forEach(Album::refreshAlbumArt); + session.getTransaction().commit(); + } + } + } diff --git a/src/main/java/musicplayer/model/Album.java b/src/main/java/musicplayer/model/Album.java index e1b676b..6b1677e 100644 --- a/src/main/java/musicplayer/model/Album.java +++ b/src/main/java/musicplayer/model/Album.java @@ -1,27 +1,71 @@ package musicplayer.model; +import javax.imageio.ImageIO; 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 -public class Album { +public class Album implements Comparable { @Id - @GeneratedValue(strategy=GenerationType.AUTO) + @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id") private long id; @Column(name = "name") private String name; + @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "album") + private Set 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; } - public String toString(){ + public String toString() { return name; } + public Set getSongs() { + return songs; + } + + public Optional 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 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") @Override public boolean equals(Object o) { @@ -41,11 +85,27 @@ public class Album { 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; } - public long getId(){ + public long getId() { return id; } + + @Override + public int compareTo(Album o) { + return toString().compareToIgnoreCase(o.toString()); + } } diff --git a/src/main/java/musicplayer/model/Artist.java b/src/main/java/musicplayer/model/Artist.java index c432e4b..403d5be 100644 --- a/src/main/java/musicplayer/model/Artist.java +++ b/src/main/java/musicplayer/model/Artist.java @@ -1,20 +1,24 @@ package musicplayer.model; import javax.persistence.*; +import java.util.Set; @Entity public class Artist { @Id - @GeneratedValue(strategy=GenerationType.AUTO) + @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id") private long id; @Column(name = "name") private String name; + @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "artist") + private Set songs; - protected Artist(){} + protected Artist() { + } - public Artist(String name){ + public Artist(String name) { this.name = name; } @@ -25,10 +29,14 @@ public class Artist { return result; } - public String toString(){ + public String toString() { return name; } + public Set getSongs() { + return songs; + } + @SuppressWarnings("MethodWithMultipleReturnPoints") @Override public boolean equals(Object o) { @@ -41,11 +49,11 @@ public class Artist { } - public String getName(){ + public String getName() { return name; } - public long getId(){ + public long getId() { return id; } } diff --git a/src/main/java/musicplayer/model/ExtractedMetadata.java b/src/main/java/musicplayer/model/ExtractedMetadata.java index 84fc2db..417452d 100644 --- a/src/main/java/musicplayer/model/ExtractedMetadata.java +++ b/src/main/java/musicplayer/model/ExtractedMetadata.java @@ -18,9 +18,9 @@ public class ExtractedMetadata { /** * @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.title = audioTags.getFirst(FieldKey.TITLE); this.album = audioTags.getFirst(FieldKey.ALBUM); diff --git a/src/main/java/musicplayer/model/Song.java b/src/main/java/musicplayer/model/Song.java index ee94014..d4871d5 100644 --- a/src/main/java/musicplayer/model/Song.java +++ b/src/main/java/musicplayer/model/Song.java @@ -5,33 +5,37 @@ import javax.persistence.*; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; +import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.NoSuchElementException; import java.util.Optional; @Entity -public class Song { +public class Song implements Comparable { @Id - @GeneratedValue(strategy=GenerationType.AUTO) + @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id") private long id; - @ManyToOne(fetch=FetchType.EAGER,cascade=CascadeType.ALL) + @ManyToOne private Artist artist; @Column(name = "genre") private String genre; @Column(name = "title") private String title; - @ManyToOne(fetch=FetchType.EAGER,cascade=CascadeType.ALL) + @ManyToOne private Album album; @Column(name = "songFile") private String songFile; @Column(name = "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.title = title; this.artist = artist; @@ -41,7 +45,7 @@ public class Song { } @Override - public String toString(){ + public String toString() { 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. + * * @return BufferedImage of album art or Optional.empty() */ - public Optional getAlbumArt(){ - try { - Optional 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) { + public Optional getAlbumArt() { + Path dir = Paths.get(songFile).getParent(); + try (DirectoryStream stream = Files.newDirectoryStream(dir, "*.{jpg,png}")) { + return Optional.of(ImageIO.read(stream.iterator().next().toFile())); + } catch (IOException | NoSuchElementException ignored) { return Optional.empty(); } } @@ -111,4 +115,11 @@ public class Song { public String getTrackNumber() { 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()); + } }