/* * Jajuk * Copyright (C) The Jajuk Team * http://jajuk.info * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * */ package org.jajuk.base; import java.awt.MediaTracker; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import javax.imageio.ImageIO; import javax.swing.ImageIcon; import org.apache.commons.lang.StringUtils; import org.jajuk.base.TrackComparator.TrackComparatorType; import org.jajuk.services.covers.Cover; import org.jajuk.services.tags.Tag; import org.jajuk.ui.thumbnails.ThumbnailManager; import org.jajuk.util.Const; import org.jajuk.util.IconLoader; import org.jajuk.util.JajukFileFilter; import org.jajuk.util.JajukIcons; import org.jajuk.util.Messages; import org.jajuk.util.UtilFeatures; import org.jajuk.util.UtilString; import org.jajuk.util.error.JajukException; import org.jajuk.util.filters.ImageFilter; import org.jajuk.util.log.Log; /** * An Album * * <p> * Logical item. */ public class Album extends LogicalItem implements Comparable<Album> { /** For perfs, we cache the associated tracks. This cache is filled by the TrackManager using the getTracksCache() method */ private final List<Track> cache = new ArrayList<Track>(15); /** This array stores thumbnail presence for all the available size (performance) By default all booleans are false. */ private boolean[] availableTumbs; /** * Album constructor. * * @param sId * @param sName * @param discID */ Album(String sId, String sName, long discID) { super(sId, sName); setProperty(Const.XML_ALBUM_DISC_ID, discID); } /** * Gets the disc id. * * @return the discID */ public long getDiscID() { return getLongValue(Const.XML_ALBUM_DISC_ID); } /** * Return album name, dealing with unknown for any language. * * @return album name */ public String getName2() { String sOut = getName(); if (sOut.equals(UNKNOWN_ALBUM)) { sOut = Messages.getString(UNKNOWN_ALBUM); } return sOut; } /** * toString method. * * @return the string */ @Override public String toString() { return "Album[ID=" + getID() + " Name={{" + getName() + "}}" + " disk ID={{" + getDiscID() + "}}]"; } /** * Alphabetical comparator on the name * <p> * Used to display ordered lists. * * @param otherAlbum * * @return comparison result */ @Override public int compareTo(Album otherAlbum) { if (otherAlbum == null) { return -1; } // compare using name and id to differentiate unknown items StringBuilder current = new StringBuilder(getName2()); current.append(getID()); StringBuilder other = new StringBuilder(otherAlbum.getName2()); other.append(otherAlbum.getID()); return current.toString().compareToIgnoreCase(other.toString()); } /** * Return whether this item is strictly unknown : contains no tag. * * @return whether this item is Unknown or not */ public boolean isUnknown() { return this.getName().equals(UNKNOWN_ALBUM); } /** * Return whether this item seems unknown (fuzzy search). * * @return whether this item seems unknown */ public boolean seemsUnknown() { return isUnknown() || "unknown".equalsIgnoreCase(getName()) || Messages.getString(UNKNOWN_ALBUM).equalsIgnoreCase(getName()); } /* * (non-Javadoc) * * @see org.jajuk.base.Item#getIdentifier() */ @Override public final String getXMLTag() { return XML_ALBUM; } /* (non-Javadoc) * @see org.jajuk.base.Item#getTitle() */ @Override public String getTitle() { return Messages.getString("Item_Album") + " : " + getName2(); } /* * (non-Javadoc) * * @see org.jajuk.base.Item#getHumanValue(java.lang.String) */ @Override public String getHumanValue(String sKey) { // We compute here all pseudo keys (non album real attributes) that can be // required on an album if (Const.XML_ARTIST.equals(sKey)) { return handleArtist(); } else if (Const.XML_ALBUM.equals(sKey)) { return getName2(); } else if (Const.XML_GENRE.equals(sKey)) { return handleGenre(); } else if (Const.XML_YEAR.equals(sKey)) { return handleYear(); } else if (Const.XML_TRACK_RATE.equals(sKey)) { return Long.toString(getRate()); } else if (Const.XML_TRACK_LENGTH.equals(sKey)) { return Long.toString(getDuration()); } else if (Const.XML_TRACKS.equals(sKey)) { return Integer.toString(getNbOfTracks()); } else if (Const.XML_TRACK_DISCOVERY_DATE.equals(sKey)) { return UtilString.getLocaleDateFormatter().format(getDiscoveryDate()); } else if (Const.XML_TRACK_HITS.equals(sKey)) { return Long.toString(getHits()); } else if (Const.XML_ANY.equals(sKey)) { return getAny(); } // default return super.getHumanValue(sKey); } /** * Handle artist. * * * @return the string */ private String handleArtist() { Artist artist = getArtist(); if (artist != null) { return artist.getName2(); } else { // More than one artist, display void string return ""; } } /** * Handle genre. * * * @return the string */ private String handleGenre() { Genre genre = getGenre(); if (genre != null) { return genre.getName2(); } else { // More than one genre, display void string return ""; } } /** * Handle year. * * * @return the string */ private String handleYear() { Year year = getYear(); if (year != null) { return Long.toString(year.getValue()); } else { return ""; } } /** * Gets the any. * * @return a human representation of all concatenated properties */ @Override public String getAny() { // rebuild any StringBuilder sb = new StringBuilder(100); sb.append(super.getAny()); // add all album-based properties // now add others properties Artist artist = getArtist(); if (artist != null) { sb.append(artist.getName2()); } // Try to add album artist Track first = null; List<Track> cache = getTracksCache(); synchronized (cache) { first = cache.get(0); } // (every track maps at minimum an "unknown artist" album artist if (first.getAlbumArtist() != null) { sb.append(first.getAlbumArtist().getName2()); } Genre genre = getGenre(); if (genre != null) { sb.append(genre.getName2()); } Year year = getYear(); if (year != null) { sb.append(getHumanValue(Const.XML_YEAR)); } sb.append(getHumanValue(Const.XML_TRACK_RATE)); sb.append(getHumanValue(Const.XML_TRACK_LENGTH)); sb.append(getHumanValue(Const.XML_TRACKS)); sb.append(getHumanValue(Const.XML_TRACK_DISCOVERY_DATE)); sb.append(getHumanValue(Const.XML_TRACK_HITS)); return sb.toString(); } /** * Gets the best associated cover as a file. * <p>Can be a long action</p> * * @return Associated best cover file available or null if none. The returned * file is not guarantee to exist, so use a try/catch around a future access to this method. */ public File findCover() { // first check if we have a selected cover that still exists String selectedCoverPath = getStringValue(XML_ALBUM_SELECTED_COVER); if (StringUtils.isNotBlank(selectedCoverPath) && new File(selectedCoverPath).exists()) { // If user-selected cover is available, just return its path return new File(selectedCoverPath); } // otherwise check if the "discovered cover" is set to "none" String discoveredCoverPath = getStringValue(XML_ALBUM_DISCOVERED_COVER); if (StringUtils.isNotBlank(discoveredCoverPath) && COVER_NONE.equals(discoveredCoverPath)) { return null; } // now check if the "discovered cover" is available if (StringUtils.isNotBlank(discoveredCoverPath)) { // Check if discovered cover still exist. There is an overhead // drawback but otherwise, the album's cover // property may be stuck to an old device's cover url. // Moreover, cover tags are extracted to cache directory so they are // Regularly dropped. Device device = DeviceManager.getInstance().getDeviceByPath(new File(discoveredCoverPath)); // If the device is not mounted, do not perform this existence check up if (device != null) { if (device.isMounted()) { if (new File(discoveredCoverPath).exists()) { return new File(discoveredCoverPath); } } else { return new File(discoveredCoverPath); } } else if (new File(discoveredCoverPath).exists()) { return new File(discoveredCoverPath); } } // None cover yet set or it is no more accessible. // Search for local covers in all directories mapping the current track // to reach other devices covers and display them together List<Track> lTracks = cache; if (lTracks.size() == 0) { return null; } // List at directories we have to look in Set<Directory> dirs = new HashSet<Directory>(2); for (Track track : lTracks) { for (org.jajuk.base.File file : track.getReadyFiles()) { // note that hashset ensures directory unicity dirs.add(file.getDirectory()); } } // If none available dir, we can't search for cover for now (may be better // next time when at least one device will be mounted) if (dirs.size() == 0) { return null; } // look for tag cover if tag supported for this type File cover = findTagCover(); // none ? look for standard cover in collection if (cover == null) { cover = findCoverFile(dirs, true); } // none ? OK, return first cover file we find if (cover == null) { cover = findCoverFile(dirs, false); } // [PERF] Still nothing ? ok, set no cover to avoid further searches if (cover == null) { setProperty(XML_ALBUM_DISCOVERED_COVER, COVER_NONE); } else { //[PERF] if we found a cover, we store it to avoid further covers // searches including a full tags picture extraction setProperty(XML_ALBUM_DISCOVERED_COVER, cover.getAbsolutePath()); } return cover; } /** * Return whether this album owns a cover (this method doesn't check * cover file existence). * @return whether this album owns a cover. */ public boolean containsCover() { String discoveredCoverPath = getStringValue(XML_ALBUM_DISCOVERED_COVER); return !StringUtils.isBlank(discoveredCoverPath) && !discoveredCoverPath.equals(COVER_NONE); } /** * Return a tag cover file from given directories. If a cover tags are found, * they are extracted to the cache directory. * * @return a tag cover file or null if none. */ private File findTagCover() { //Make sure to sort the cache List<Track> sortedTracks = new ArrayList<Track>(cache); Collections.sort(sortedTracks, new TrackComparator(TrackComparatorType.ALBUM)); for (Track track : sortedTracks) { for (org.jajuk.base.File file : track.getReadyFiles()) { try { if (file != null && file.getType() != null && file.getType().getTagImpl() != null) { Tag tag = new Tag(file.getFIO(), false); List<Cover> covers = tag.getCovers(); if (covers.size() > 0) { return covers.get(0).getFile(); } } } catch (JajukException e1) { Log.error(e1); } } } return null; } /** * Return a cover file matching criteria or null. * * @param dirs : list of directories to search in * @param onlyStandardCovers to we consider only standard covers ? * * @return a cover file matching criteria or null */ private File findCoverFile(Set<Directory> dirs, boolean onlyStandardCovers) { JajukFileFilter filter = new JajukFileFilter(ImageFilter.getInstance()); for (Directory dir : dirs) { File fDir = dir.getFio(); // store this dir java.io.File[] files = fDir.listFiles();// null if none file // found for (int i = 0; files != null && i < files.length; i++) { if (files[i].exists() // check size to avoid out of memory errors && files[i].length() < MAX_COVER_SIZE * 1024 // Is it an image ? && filter.accept(files[i])) { // Filter standard view if required if (onlyStandardCovers && !UtilFeatures.isStandardCover(files[i])) { continue; } // Test the image is not corrupted try { ImageIcon ii = new ImageIcon(files[i].getAbsolutePath()); // Note that at this point, the image is fully loaded (done in the ImageIcon // constructor) if (ii.getImageLoadStatus() == MediaTracker.COMPLETE) { return files[i]; } else { Log.debug("Problem loading: " + files[i].getAbsolutePath()); } } catch (Exception e) { Log.error(e); } } } } return null; } /* * (non-Javadoc) * * @see org.jajuk.base.Item#getIconRepresentation() */ @Override public ImageIcon getIconRepresentation() { return IconLoader.getIcon(JajukIcons.ALBUM); } /** * Gets the rate. * * @return album rating */ @Override public long getRate() { long rate = 0; for (Track track : cache) { rate += track.getRate(); } return rate; } /** * Gets the thumbnail. * * @param size size using format width x height * * @return album thumb for given size */ public ImageIcon getThumbnail(int size) { File fCover = ThumbnailManager.getThumbBySize(this, size); // Check if thumb already exists if (!fCover.exists() || fCover.length() == 0) { return IconLoader.getNoCoverIcon(size); } BufferedImage img = null; try { img = ImageIO.read(new File(fCover.getAbsolutePath())); } catch (IOException e) { Log.error(e); } // can be null now if an error occurred, we reported a error to the log // already... if (img == null) { return null; } ImageIcon icon = new ImageIcon(img); // Free thumb memory (DO IT AFTER FULL ImageIcon loading) img.flush(); return icon; } /** * Gets the genre. * * @return genre for the album. Return null if the album contains tracks with * different genres */ public Genre getGenre() { Set<Genre> genres = new HashSet<Genre>(1); for (Track track : cache) { genres.add(track.getGenre()); } // If different genres, the album genre is null if (genres.size() == 1) { return genres.iterator().next(); } else { return null; } } /** * Gets the artist. * * @return artist for the album. <br> * Return null if the album contains tracks with different artists */ public Artist getArtist() { if (cache.size() == 0) { return null; } Artist first = cache.get(0).getArtist(); for (Track track : cache) { if (!track.getArtist().equals(first)) { return null; } } return first; } /** * Gets the artist or the album artist if not available * * <u>Used algorithm is following : * <li>If none available tags : return "unknown artist"</li> * <li>If the album contains tracks with different artists, display the first album artist found if any</li> * <li>In this case, if no album artist is available, display the first artist found</li> * </u>. * * @return artist for the album. <br> * Return Always an artist, eventually a "Unknown Artist" one */ public String getArtistOrALbumArtist() { // no track => no artist if (cache.size() == 0) { return Const.UNKNOWN_ARTIST; } Artist artist = getArtist(); if (artist != null && !artist.isUnknown()) { return artist.getName(); } else { Track first = cache.get(0); AlbumArtist albumArtist = first.getAlbumArtist(); if (!albumArtist.isUnknown()) { return albumArtist.getName(); } else { return first.getArtist().getName(); } } } /** * Gets the year. * * @return year for the album. Return null if the album contains tracks with * different years */ public Year getYear() { Set<Year> years = new HashSet<Year>(1); for (Track track : cache) { years.add(track.getYear()); } // If different Artists, the album Artist is null if (years.size() == 1) { return years.iterator().next(); } else { return null; } } /** * Return full album length in secs. * * @return the duration */ public long getDuration() { long length = 0; for (Track track : cache) { length += track.getDuration(); } return length; } /** * Gets the nb of tracks. * * @return album nb of tracks */ public int getNbOfTracks() { return cache.size(); } /** * Gets the hits. * * @return album total nb of hits */ public long getHits() { int hits = 0; for (Track track : cache) { hits += track.getHits(); } return hits; } /** * Contains ready files. * * @return whether the album contains a least one available track */ public boolean containsReadyFiles() { for (Track track : cache) { if (track.getReadyFiles().size() > 0) { return true; } } return false; } /** * Gets the discovery date. * * @return First found track discovery date */ public Date getDiscoveryDate() { if (cache.size() > 0) { return cache.get(0).getDiscoveryDate(); } else { return null; } } /** * Gets the tracks cache. * * @return ordered tracks cache for this album (perf) */ public List<Track> getTracksCache() { return this.cache; } /** * Gets the any track. * * @return a track from this album */ public Track getAnyTrack() { if (cache.size() == 0) { return null; } else { return cache.get(0); } } /** * Set that the thumb for given size is available. * * @param size (thumb size like 50) * @param available */ public void setAvailableThumb(int size, boolean available) { if (availableTumbs == null) { availableTumbs = new boolean[6]; } availableTumbs[size / 50 - 1] = available; } /** * Return whether a thumb is available for given size. * * @param size (thumb size like 50) * * @return whether a thumb is available for given size */ public boolean isThumbAvailable(int size) { // Lazy loading of thumb availability (for all sizes) if (availableTumbs == null) { availableTumbs = new boolean[6]; for (int i = 50; i <= 300; i += 50) { File fThumb = ThumbnailManager.getThumbBySize(this, i); setAvailableThumb(i, fThumb.exists() && fThumb.length() > 0); } } return availableTumbs[size / 50 - 1]; } /** * Force any new cover search before displaying it if the album is set "none" cover (for example, if the album contains no cover at all, * the album is stuck as NONE_COVER while a thumb refresh is not done manually by the user). * If a new cover is added from outside jajuk and no save or save as action is done, the new thumb is not built from the new cover so we force it. */ public void resetCoverCache() { String cachedCoverPath = getStringValue(Const.XML_ALBUM_DISCOVERED_COVER); if (Const.COVER_NONE.equals(cachedCoverPath)) { setProperty(Const.XML_ALBUM_DISCOVERED_COVER, ""); } } /** * Cleanup orphan tracks */ protected void cleanupCache() { synchronized (cache) { Iterator<Track> it = cache.iterator(); while (it.hasNext()) { Track track = it.next(); if (track.getFiles().size() == 0) { it.remove(); } } } } }