/* * 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.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.Set; import javax.swing.ImageIcon; import org.apache.commons.lang.StringUtils; import org.jajuk.services.core.RatingService; import org.jajuk.util.Conf; import org.jajuk.util.Const; import org.jajuk.util.IconLoader; import org.jajuk.util.JajukIcons; import org.jajuk.util.Messages; import org.jajuk.util.UtilString; import org.jajuk.util.log.Log; /** * A track * <p> * Logical item. */ public class Track extends LogicalItem implements Comparable<Track> { /** Track album*. */ private final Album album; /** Track genre. */ private final Genre genre; /** Track artist. */ private final Artist artist; /** Track length. */ private final long length; /** Track year. */ private final Year year; /** Track type. */ private final Type type; /** Album Artist. */ private AlbumArtist albumArtist; /** Track associated files. */ private final List<File> alFiles = new ArrayList<File>(1); /** * Track constructor. * * @param sId * @param sName * @param album * @param genre * @param artist * @param length * @param year * @param lOrder * @param type * @param lDiscNumber */ Track(String sId, String sName, Album album, Genre genre, Artist artist, long length, Year year, long lOrder, Type type, long lDiscNumber) { super(sId, sName); // album this.album = album; setProperty(Const.XML_ALBUM, album.getID()); // genre this.genre = genre; setProperty(Const.XML_GENRE, genre.getID()); // artist this.artist = artist; setProperty(Const.XML_ARTIST, artist.getID()); // Length this.length = length; setProperty(Const.XML_TRACK_LENGTH, length); // Type this.type = type; setProperty(Const.XML_TYPE, type.getID()); // Year this.year = year; setProperty(Const.XML_YEAR, year.getID()); // Order setProperty(Const.XML_TRACK_ORDER, lOrder); // Order setProperty(Const.XML_TRACK_DISC_NUMBER, lDiscNumber); // Rate setProperty(Const.XML_TRACK_RATE, 0l); // Hits setProperty(Const.XML_TRACK_HITS, 0l); } /** * toString method. * * @return the string */ @Override public String toString() { StringBuilder sOut = new StringBuilder(); sOut.append("Track[ID=").append(getID()).append(" Name={{").append(getName()).append("}} ") .append(album).append(" ").append(genre).append(" ").append(artist).append(" Length=") .append(length).append(" Year=").append(year.getValue()).append(" Rate=").append(getRate()) .append(" ").append(type).append(" Hits=").append(getHits()).append(" Addition date=") .append(getDiscoveryDate()).append(" Comment=").append(getComment()).append(" order=") .append(getOrder()).append(" Nb of files=").append(alFiles.size()).append(" Album artist=") .append(getAlbumArtist()).append(" Disc=").append(getDiscNumber()).append("]"); for (int i = 0; i < alFiles.size(); i++) { sOut.append('\n').append(alFiles.get(i).toString()); } return sOut.toString(); } /** * 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 track-based properties // Add all files absolute paths for (File file : getFiles()) { sb.append(file.getAbsolutePath()); } return sb.toString(); } /** * Default comparator for tracks, not used for actual sorting (use TrackComparator * for that), only for storage purpose. * * @param otherTrack * * @return comparison result */ @Override public int compareTo(Track otherTrack) { return getID().compareTo(otherTrack.getID()); } /** * Gets the album. * * @return the album */ public Album getAlbum() { return album; } /** * Gets a copy of associated files. * * @return a copy of associated files */ public List<org.jajuk.base.File> getFiles() { return new ArrayList<File>(alFiles); } /** * Remove specified file from associated files. * * @param file : the file to remove */ void removeFile(File file) { alFiles.remove(file); } /** * Gets the ready files. * * @return ready files */ public List<File> getReadyFiles() { List<File> alReadyFiles = new ArrayList<File>(alFiles.size()); for (File file : alFiles) { if (file.isReady()) { alReadyFiles.add(file); } } return alReadyFiles; } /** * Gets the ready files. * * @param filter files we want to deal with, null means no filter * * @return ready files with given filter */ List<File> getReadyFiles(Set<File> filter) { List<File> alReadyFiles = new ArrayList<File>(alFiles.size()); for (File file : alFiles) { if (file.isReady() && (filter == null || filter.contains(file))) { alReadyFiles.add(file); } } return alReadyFiles; } /** * Get sum size of all files this track map to. * * @return the total size */ public long getTotalSize() { long l = 0; for (final File file : alFiles) { l += file.getSize(); } return l; } /** * Gets the playable file. * * @param bIgnoreUnmounted Do we return unmounted files * * @return best file to play for this track or null if none available */ public File getBestFile(boolean bIgnoreUnmounted) { File fileOut = null; final List<File> alMountedFiles = new ArrayList<File>(2); // firstly, filter mounted files if needed for (final File file : alFiles) { if (!bIgnoreUnmounted || file.isReady()) { alMountedFiles.add(file); } } if (alMountedFiles.size() == 1) { fileOut = alMountedFiles.get(0); } else if (alMountedFiles.size() > 0) { // then keep best quality and mounted first Collections.sort(alMountedFiles, new Comparator<File>() { @Override public int compare(File file1, File file2) { long lQuality1 = file1.getQuality(); boolean bMounted1 = file1.isReady(); long lQuality2 = file2.getQuality(); // quality for // out file boolean bMounted2 = file2.isReady(); if (bMounted1 && !bMounted2) {// first item mounted, // not second return 1; } else if (!bMounted1 && bMounted2) { // second // mounted, not // the first return -1; } else { // both mounted or unmounted, compare quality return (int) (lQuality1 - lQuality2); } } }); fileOut = alMountedFiles.get(alMountedFiles.size() - 1); } return fileOut; } /** * Gets the hits. * * @return the hits */ public long getHits() { return getLongValue(Const.XML_TRACK_HITS); } /** * Gets the comment. * * @return the comment */ public String getComment() { return getStringValue(Const.XML_TRACK_COMMENT); } /** * Get track number. * * @return the order */ public long getOrder() { return getLongValue(Const.XML_TRACK_ORDER); } /** * Get disc number. * * @return the disc number */ public long getDiscNumber() { return getLongValue(Const.XML_TRACK_DISC_NUMBER); } /** * Get album artist. * * @return the album artist */ public AlbumArtist getAlbumArtist() { return albumArtist; } /** * Gets the album artist or artist if album-artist is not available. * * @return the albumArtist or artist if album artist not available * <p> * If this is various, the album artist is tried to be defined by the * track artists of this album * </p> */ public String getAlbumArtistOrArtist() { // If the album artist tag is provided, perfect, let's use it ! AlbumArtist albumArtist = getAlbumArtist(); if (albumArtist != null && StringUtils.isNotBlank(albumArtist.getName()) && !(Const.UNKNOWN_ARTIST.equals(albumArtist.getName()))) { return albumArtist.getName(); } // various artist? check if all artists are the same Artist artist = getArtist(); if (artist == null) { // Several different artist, return translated "various" return Messages.getString(Const.VARIOUS_ARTIST); } else { // single artist, return it return artist.getName2(); } } /** * Gets the year. * * @return the year */ public Year getYear() { return year; } /** * Gets the duration. * * @return length in sec */ public long getDuration() { return length; } /* (non-Javadoc) * @see org.jajuk.base.Item#getRate() */ @Override public long getRate() { return getLongValue(Const.XML_TRACK_RATE); } /** * Gets the discovery date. * * @return the date where the track has been discovered (added into the * collection) */ public Date getDiscoveryDate() { return getDateValue(Const.XML_TRACK_DISCOVERY_DATE); } /** * Gets the type. * * @return the type */ public Type getType() { return type; } /** * Gets the artist. * * @return the artist */ public Artist getArtist() { return artist; } /** * Gets the genre. * * @return the genre */ public Genre getGenre() { return genre; } /** * Add an associated file. * * @param file */ public void addFile(File file) { // make sure a file will be referenced by only one track (first found) if (!alFiles.contains(file) && file.getTrack().equals(this)) { alFiles.add(file); } } /** * Sets the hits. * * @param hits The iHits to set. */ public void setHits(long hits) { setProperty(Const.XML_TRACK_HITS, hits); // Store max playcount if (hits > RatingService.getMaxPlaycount()) { RatingService.setMaxPlaycount(hits); } } /** * Increase playcount number. */ public void incHits() { long value = getHits() + 1; setHits(value); } /** * Set track preference (from -3 to 3: -3: hate, -2=dislike, -1=poor, +1=like, * +2=love +3=crazy). The preference is a factor given by the user to increase * or decrease a track rate. * * @param preference from -3 to 3 */ public void setPreference(long preference) { Log.debug("Changed preference of " + getID() + "=" + preference); if (preference >= -3l && preference <= 3l) { setProperty(Const.XML_TRACK_PREFERENCE, preference); } else { setProperty(Const.XML_TRACK_PREFERENCE, 0l); Log.debug("Out of bounds preference for : " + getID()); } updateRate(); } /** * Compute final track rate. * * @see #1179 */ public void updateRate() { try { // -- Manual rating : just use preference if (Conf.getBoolean(Const.CONF_MANUAL_RATINGS)) { long preference = getLongValue(Const.XML_TRACK_PREFERENCE); long rate = RatingService.getRateForPreference(preference); setRate(rate); // -- Semi-Automatic (standard) rating } else { // rate contains final rate [0,100] long rate = 0; // Normalize values to avoid division by zero long duration = getDuration(); long playcount = getHits(); // Playcount must be > 0 to avoid divisions by zero and log(0) operations if (playcount <= 0) { playcount = 1; } float playtimeRate = 0.5f; if (duration == 0) { // If duration = 0, always set playtimeRate to 0.5 Log.info("Duration = 0 for: {{" + getName() + "}}. Playtime forced to 0.5"); } else { // Compute playtime rate = total play time / (play count * track length) playtimeRate = (float) getLongValue(Const.XML_TRACK_TOTAL_PLAYTIME) / (playcount * duration); } // playtimeRate can be > 1 because of player impl duration computation // precision issue or if user seeks back into the track // set =1. if (playtimeRate > 1) { Log.warn("Playtime rate > 1 for: {{" + getName() + "}} value=" + playtimeRate); // We reset tpt and hits to // make things clear and to avoid increasing the error with time setProperty(Const.XML_TRACK_TOTAL_PLAYTIME, duration * playcount); playtimeRate = 1f; } // compute the playcount rate (logarithmic scale to take number of plays // into account) // playcountRate = ln(track playcount)/ln(max playcount) long maxPlayCount = RatingService.getMaxPlaycount(); if (maxPlayCount <= 0) { maxPlayCount = 1; } float playcountRate = (float) (Math.log(playcount) / Math.log(maxPlayCount)); // Intermediate rate is a mix between playtime and playcount rates with // factor 0.75 for the first one and 0.25 for the second float intermediateRate = (0.75f * playtimeRate) + (0.25f * playcountRate); // Final rate is intermediateRate in whish we apply the user preference // from // -3 (hate) to 3 (adore) long preference = getLongValue(Const.XML_TRACK_PREFERENCE); long absPreference = Math.abs(preference); rate = Math.round(100 * (intermediateRate + (preference + absPreference) / 2) / (absPreference + 1)); // Apply new rate setRate(rate); } } catch (Exception e) { // We catch any arithmetic issue here to avoid preventing next track // startup Log.error(e); } } /** * Sets the rate. * * @param rate The lRate to set. */ protected void setRate(long rate) { setProperty(Const.XML_TRACK_RATE, rate); RatingService.setRateHasChanged(true); } /** * Sets the comment. * * @param sComment */ public void setComment(String sComment) { setProperty(Const.XML_TRACK_COMMENT, sComment); } /** * Sets the album artist. * * @param albumArtist : the album artist */ public void setAlbumArtist(AlbumArtist albumArtist) { this.albumArtist = albumArtist; // We store the album-artist ID string, not the album-artist itself setProperty(Const.XML_ALBUM_ARTIST, albumArtist.getID()); } /** * Sets the discovery date. * * @param additionDate The sAdditionDate to set. */ public void setDiscoveryDate(Date additionDate) { setProperty(Const.XML_TRACK_DISCOVERY_DATE, additionDate); } /** * Return whether this item should be hidden with hide option. * * @return whether this item should be hidden with hide option */ public boolean shouldBeHidden() { if (getBestFile(true) != null || !Conf.getBoolean(Const.CONF_OPTIONS_HIDE_UNMOUNTED)) { return false; } return true; } /* * (non-Javadoc) * * @see org.jajuk.base.Item#getIdentifier() */ @Override public final String getXMLTag() { return XML_TRACK; } /* (non-Javadoc) * @see org.jajuk.base.Item#getTitle() */ @Override public String getTitle() { return Messages.getString("Item_Track") + " : " + getName(); } /* * (non-Javadoc) * * @see org.jajuk.base.Item#getHumanValue(java.lang.String) */ @Override public String getHumanValue(String sKey) { if (Const.XML_ALBUM.equals(sKey)) { Album lAlbum = AlbumManager.getInstance().getAlbumByID(getStringValue(sKey)); if (lAlbum != null) { // can be null after a fresh change return lAlbum.getName2(); } return null; } else if (Const.XML_ARTIST.equals(sKey)) { Artist artist = ArtistManager.getInstance().getArtistByID(getStringValue(sKey)); if (artist != null) { // can be null after a fresh change return artist.getName2(); } return null; } else if (Const.XML_ALBUM_ARTIST.equals(sKey)) { AlbumArtist albumArtist = AlbumArtistManager.getInstance().getAlbumArtistByID( getStringValue(sKey)); if (albumArtist != null) { // can be null after a fresh change return albumArtist.getName2(); } return null; } else if (Const.XML_GENRE.equals(sKey)) { Genre genre = GenreManager.getInstance().getGenreByID(getStringValue(sKey)); if (genre != null) { // can be null after a fresh change return genre.getName2(); } return null; } else if (Const.XML_TRACK_LENGTH.equals(sKey)) { return UtilString.formatTimeBySec(length); } else if (Const.XML_TYPE.equals(sKey)) { return TypeManager.getInstance().getTypeByID(getStringValue(sKey)).getName(); } else if (Const.XML_YEAR.equals(sKey)) { return getStringValue(sKey); } else if (Const.XML_FILES.equals(sKey)) { final StringBuilder sbOut = new StringBuilder(); for (final File file : alFiles) { sbOut.append(file.getAbsolutePath()); sbOut.append(','); } return sbOut.substring(0, sbOut.length() - 1); // remove trailing coma } else if (Const.XML_TRACK_DISCOVERY_DATE.equals(sKey)) { return UtilString.getLocaleDateFormatter().format(getDiscoveryDate()); } else if (Const.XML_ANY.equals(sKey)) { return getAny(); } else {// default return super.getHumanValue(sKey); } } /* * (non-Javadoc) * * @see org.jajuk.base.Item#getIconRepresentation() */ @Override public ImageIcon getIconRepresentation() { return IconLoader.getIcon(JajukIcons.TRACK); } /** * Gets the files string. * * @return a list of associated files in format : file1,file2... */ public String getFilesString() { StringBuilder sb = new StringBuilder(100); for (File file : alFiles) { sb.append(file.getName()); sb.append(','); } // Remove trailing ',' sb.deleteCharAt(sb.length() - 1); return sb.toString(); } }