/*
* 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.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jajuk.base.TrackComparator.TrackComparatorType;
import org.jajuk.events.JajukEvent;
import org.jajuk.events.JajukEvents;
import org.jajuk.events.ObservationManager;
import org.jajuk.events.Observer;
import org.jajuk.services.players.QueueModel;
import org.jajuk.util.Const;
import org.jajuk.util.MD5Processor;
import org.jajuk.util.ReadOnlyIterator;
import org.jajuk.util.error.JajukException;
/**
* Convenient class to manage Albums.
*/
public final class AlbumManager extends ItemManager implements Observer {
/** Self instance. */
private static AlbumManager singleton = new AlbumManager();
/** Album max rating. */
private long maxRate = 0l;
private int comp = 0;
/**
* No constructor available, only static access.
*/
private AlbumManager() {
super();
// register properties
// ID
registerProperty(new PropertyMetaInformation(Const.XML_ID, false, true, false, false, false,
String.class, null));
// Name
registerProperty(new PropertyMetaInformation(Const.XML_NAME, false, true, true, true, false,
String.class, null));
// Expand
registerProperty(new PropertyMetaInformation(Const.XML_EXPANDED, false, false, false, false,
true, Boolean.class, false));
// Discovered Cover path
registerProperty(new PropertyMetaInformation(Const.XML_ALBUM_DISCOVERED_COVER, false, false,
false, false, false, String.class, null));
// Selected Cover path
registerProperty(new PropertyMetaInformation(Const.XML_ALBUM_SELECTED_COVER, false, false,
false, false, false, String.class, null));
// Disc id
registerProperty(new PropertyMetaInformation(Const.XML_ALBUM_DISC_ID, false, true, false,
false, false, Long.class, -1l));
// Register events
ObservationManager.register(this);
}
/*
* (non-Javadoc)
*
* @see org.jajuk.events.Observer#getRegistrationKeys()
*/
@Override
public Set<JajukEvents> getRegistrationKeys() {
Set<JajukEvents> eventSubjectSet = new HashSet<JajukEvents>();
eventSubjectSet.add(JajukEvents.FILE_LAUNCHED);
return eventSubjectSet;
}
/**
* Gets the instance.
*
* @return singleton
*/
public static AlbumManager getInstance() {
return singleton;
}
/**
* Return hashcode for this item.
*
* @param sName item name
* @param discId
*
* @return ItemManager ID
*/
protected static String createID(String sName, long discId) {
return MD5Processor.hash(sName + discId);
}
/**
* Register an Album with a known id.
*
* @param sId
* @param sName
* @param discID
* @return the album
*/
public Album registerAlbum(String sId, String sName, long discID) {
Album album = getAlbumByID(sId);
if (album != null) {
return album;
}
album = new Album(sId, sName, discID);
registerItem(album);
return album;
}
/**
* Register an Album.
*
* @param sName
* @param discID
*
* @return the album
*/
public Album registerAlbum(String sName, long discID) {
String sId = createID(sName, discID);
return registerAlbum(sId, sName, discID);
}
/**
* Change the item.
*
* @param old
* @param sNewName
*
* @return new album
*
* @throws JajukException the jajuk exception
*/
Album changeAlbumName(Album old, String sNewName) throws JajukException {
// check there is actually a change
if (old.getName2().equals(sNewName)) {
return old;
}
// check up front as later the state of the track is already changed
boolean bQueueUpdateRequired = false;
if (QueueModel.getPlayingFile() != null
&& QueueModel.getPlayingFile().getTrack().getAlbum().equals(old)) {
bQueueUpdateRequired = true;
}
Album newItem = registerAlbum(sNewName, old.getDiscID());
// re apply old properties from old item
newItem.cloneProperties(old);
// update tracks
for (Track track : TrackManager.getInstance().getTracks()) {
if (track.getAlbum().equals(old)) {
TrackManager.getInstance().changeTrackAlbum(track, sNewName, null);
}
}
// if current track album name is changed, notify it
if (bQueueUpdateRequired) {
ObservationManager.notify(new JajukEvent(JajukEvents.ALBUM_CHANGED));
}
// remove old item
removeItem(old);
return newItem;
}
/**
* Format the album name to be normalized :
* <p>
* -no underscores or other non-ASCII characters
* <p>
* -no spaces at the begin and the end
* <p>
* -All in lower case expect first letter of first word
* <p>
* example: "My album title".
*
* @param sName The name to format.
*
* @return The formatted string.
*/
public static String format(String sName) {
String sOut = sName.trim(); // suppress spaces at the begin and the end
sOut = sOut.replace('-', ' '); // move - to space
sOut = sOut.replace('_', ' '); // move _ to space
char c = sOut.charAt(0);
StringBuilder sb = new StringBuilder(sOut);
sb.setCharAt(0, Character.toUpperCase(c));
return sb.toString();
}
/*
* (non-Javadoc)
*
* @see org.jajuk.base.ItemManager#getIdentifier()
*/
@Override
public String getXMLTag() {
return Const.XML_ALBUMS;
}
/**
* Gets the album by id.
*
* @param sID Item ID
*
* @return Element
*/
Album getAlbumByID(String sID) {
return (Album) getItemByID(sID);
}
/**
* Gets the albums.
*
* @return ordered albums list
*/
@SuppressWarnings("unchecked")
public List<Album> getAlbums() {
return (List<Album>) getItems();
}
/**
* Gets the albums iterator.
*
* @return albums iterator
*/
@SuppressWarnings("unchecked")
public ReadOnlyIterator<Album> getAlbumsIterator() {
return new ReadOnlyIterator<Album>((Iterator<Album>) getItemsIterator());
}
/**
* Get sorted list of albums associated with this item.
*
* @param item the item
*
* @return a list of item, void list if no result
*/
public List<Album> getAssociatedAlbums(Item item) {
List<Album> out;
// [Perf] If item is a track, just return its album
if (item instanceof Track) {
out = new ArrayList<Album>(1);
out.add(((Track) item).getAlbum());
} else {
try {
lock.readLock().lock();
ReadOnlyIterator<Album> albums = getAlbumsIterator();
// Use a set to avoid dups
Set<Album> albumSet = new HashSet<Album>();
while (albums.hasNext()) {
Album album = albums.next();
List<Track> cache = album.getTracksCache();
synchronized (cache) {
for (Track track : cache) {
if (item instanceof Artist && track.getArtist().equals(item)) {
albumSet.add(album);
} else if (item instanceof Genre && track.getGenre().equals(item)) {
albumSet.add(album);
} else if (item instanceof Year && track.getYear().equals(item)) {
albumSet.add(album);
}
}
}
}
out = new ArrayList<Album>(albumSet);
Collections.sort(out);
} finally {
lock.readLock().unlock();
}
}
return out;
}
@Override
public void cleanup() {
for (Item item : getItems()) {
((Album) item).cleanupCache();
}
super.cleanup();
}
/**
* Return sorted top albums based on the average of each album rating.
*
* @param bHideUnmounted if true, unmounted albums are not chosen
* @param iNbBestofAlbums nb of items to return
*
* @return top albums, can be less items than required according to nb of
* available albums
*/
public List<Album> getBestOfAlbums(boolean bHideUnmounted, int iNbBestofAlbums) {
lock.readLock().lock();
try {
// Create a temporary table to remove unmounted albums
// We consider an album as mounted if a least one track is mounted
// This hashmap contains album-> album rates
final Map<Album, Float> cacheRate = new HashMap<Album, Float>(AlbumManager.getInstance()
.getElementCount());
ReadOnlyIterator<Album> it = AlbumManager.getInstance().getAlbumsIterator();
while (it.hasNext()) {
Album album = it.next();
if (!bHideUnmounted || album.containsReadyFiles()) {
cacheRate.put(album, (float) album.getRate());
}
}
// Now sort albums by rating
List<Album> sortedAlbums = new ArrayList<Album>(cacheRate.keySet());
Collections.sort(sortedAlbums, new Comparator<Album>() {
@Override
public int compare(Album o1, Album o2) {
// lowest first
return (int) (cacheRate.get(o1) - cacheRate.get(o2));
}
});
return getTopAlbums(sortedAlbums, iNbBestofAlbums);
} finally {
lock.readLock().unlock();
}
}
/**
* Return ordered list of newest albums.
*
* @param bHideUnmounted if true, unmounted albums are not chosen
* @param iNb nb of items to return
*
* @return newest albums
*/
public List<Album> getNewestAlbums(boolean bHideUnmounted, int iNb) {
lock.readLock().lock();
try {
// create a temporary table to remove unmounted albums
// We consider an album as mounted if a least one track is mounted
// This hashmap contains album-> discovery date
final Map<Album, Date> cache = new HashMap<Album, Date>(AlbumManager.getInstance()
.getElementCount());
ReadOnlyIterator<Track> it = TrackManager.getInstance().getTracksIterator();
while (it.hasNext()) {
Track track = it.next();
if (track.getBestFile(bHideUnmounted) != null) {
cache.put(track.getAlbum(), track.getDiscoveryDate());
}
}
// Now sort albums by discovery date
List<Album> sortedAlbums = new ArrayList<Album>(cache.keySet());
Collections.sort(sortedAlbums, new Comparator<Album>() {
@Override
public int compare(Album o1, Album o2) {
return cache.get(o1).compareTo(cache.get(o2));
}
});
return getTopAlbums(sortedAlbums, iNb);
} finally {
lock.readLock().unlock();
}
}
/**
* Return ordered rarely listen albums list.
*
* @param bHideUnmounted if true, unmounted albums are not chosen
* @param iNb nb of items to return
*
* @return top albums, can be less items than required according to nb of
* available albums
*/
public List<Album> getRarelyListenAlbums(boolean bHideUnmounted, int iNb) {
lock.readLock().lock();
try {
// create a temporary table to remove unmounted albums
// We consider an album as mounted if a least one track is mounted
// This hashmap contains album-> album hits (each track hit average)
final Map<Album, Float> cache = new HashMap<Album, Float>(AlbumManager.getInstance()
.getElementCount());
// This hashmap contains album-> nb of tracks already taken into account
// for average
Map<Album, Integer> cacheNb = new HashMap<Album, Integer>(AlbumManager.getInstance()
.getElementCount());
ReadOnlyIterator<Track> it = TrackManager.getInstance().getTracksIterator();
while (it.hasNext()) {
Track track = it.next();
if (track.getBestFile(bHideUnmounted) != null) {
float newHits = 0f;
Integer nb = cacheNb.get(track.getAlbum());
if (nb == null) {
nb = 0;
}
Float previousRate = cache.get(track.getAlbum());
if (previousRate == null) {
newHits = track.getHits();
} else {
newHits = ((previousRate * nb) + track.getHits()) / (nb + 1);
}
cacheNb.put(track.getAlbum(), nb + 1);
cache.put(track.getAlbum(), newHits);
}
}
// Now sort albums by rating
List<Album> sortedAlbums = new ArrayList<Album>(cache.keySet());
Collections.sort(sortedAlbums, new Comparator<Album>() {
@Override
public int compare(Album o1, Album o2) {
// We inverse comparison as we want lowest scores
return (int) (cache.get(o2) - cache.get(o1));
}
});
return getTopAlbums(sortedAlbums, iNb);
} finally {
lock.readLock().unlock();
}
}
/**
* Convenient method to keep top albums (used by getBestof, newest... albums)
*
* @param sortedAlbums sorted albums according desired criteria, size >= iNb
* @param iNb Number of albums to return
*
* @return a nicely sorted / shuffled list of albums or a void list of none
* available albums
*/
private List<Album> getTopAlbums(List<Album> sortedAlbums, int iNb) {
// Keep only 3 * desired size or less if not enough available albums
int size = 2 * iNb;
if (sortedAlbums.size() <= size) {
size = sortedAlbums.size() - 1;
}
// Leave if none album so far
if (sortedAlbums.size() == 0) {
return new ArrayList<Album>();
}
List<Album> sublist = sortedAlbums.subList(sortedAlbums.size() - (1 + size),
sortedAlbums.size() - 1);
// Shuffle the result
Collections.shuffle(sublist);
// The result is a sublist of shuffled albums, if we have less
// albums than required, take max size possible
return sublist.subList(0, (size >= iNb) ? iNb : size);
}
/**
* Gets the max rate.
*
* @return max rating for an album
*/
public long getMaxRate() {
return this.maxRate;
}
/**
* Force to refresh the album max rating, it is not done soon as it is pretty
* CPU consuming and we don't need a track by track rating precision.
*/
public void refreshMaxRating() {
// create a temporary table to remove unmounted albums
// We consider an album as mounted if a least one track is mounted
// This hashmap contains album-> album rates
final Map<Album, Float> cacheRate = new HashMap<Album, Float>(AlbumManager.getInstance()
.getElementCount());
for (Album album : AlbumManager.getInstance().getAlbums()) {
cacheRate.put(album, (float) album.getRate());
}
// OK, now keep only the highest score
for (Map.Entry<Album, Float> album : cacheRate.entrySet()) {
long value = Math.round(album.getValue());
if (value > maxRate) {
maxRate = value;
}
}
}
/*
* (non-Javadoc)
*
* @see org.jajuk.base.Observer#update(org.jajuk.base.Event)
*/
@Override
public void update(JajukEvent event) {
if (event.getSubject() == JajukEvents.FILE_LAUNCHED) {
// Compute album max rating every 10 tracks launches
if (comp % 10 == 0) {
refreshMaxRating();
}
comp++;
}
}
/**
* Gets the album by name.
*
* @param name
*
* @return associated album (case insensitive) or null if no match
*/
public Album getAlbumByName(String name) {
lock.readLock().lock();
try {
Album out = null;
for (ReadOnlyIterator<Album> it = getAlbumsIterator(); it.hasNext();) {
Album album = it.next();
if (album.getName().equalsIgnoreCase(name)) {
out = album;
break;
}
}
return out;
} finally {
lock.readLock().unlock();
}
}
/**
* Specialize switchToOrderState, here we sort the album cache in addition.
*/
public void orderCache() {
// read lock, not write lock because we need a write lock only when performing
// structural changes to items collection
for (Album album : getAlbums()) {
List<Track> cache = album.getTracksCache();
synchronized (cache) {
Collections.sort(cache, new TrackComparator(TrackComparatorType.ALBUM));
}
}
}
/**
* Reset cached cover url for every album.
* Note that we reset only cached/discovered covers, not user-selected covers.
*/
public void resetCoverCache() {
for (Album album : getAlbums()) {
album.setProperty(Const.XML_ALBUM_DISCOVERED_COVER, "");
}
}
}