/*
* 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();
}
}
}
}
}