/*
* 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.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import javax.swing.JOptionPane;
import org.jajuk.base.TrackComparator.TrackComparatorType;
import org.jajuk.events.JajukEvent;
import org.jajuk.events.JajukEvents;
import org.jajuk.events.ObservationManager;
import org.jajuk.services.players.QueueModel;
import org.jajuk.services.tags.Tag;
import org.jajuk.util.Conf;
import org.jajuk.util.Const;
import org.jajuk.util.MD5Processor;
import org.jajuk.util.Messages;
import org.jajuk.util.ReadOnlyIterator;
import org.jajuk.util.UtilString;
import org.jajuk.util.error.JajukException;
import org.jajuk.util.error.NoneAccessibleFileException;
import org.jajuk.util.log.Log;
/**
* Convenient class to manage Tracks.
*/
public final class TrackManager extends ItemManager {
/** Self instance. */
private static TrackManager singleton = new TrackManager();
/** Autocommit flag for tags *. */
private volatile boolean bAutocommit = true;
/** Set of tags to commit. */
private final Set<Tag> tagsToCommit = new HashSet<Tag>(10);
/** Attic for tracks that have been dropped but may be useful when a file is renamed to restore its properties */
private final Map<String, Track> attic = new HashMap<String, Track>(0);
/**
* No constructor available, only static access.
*/
private TrackManager() {
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));
// Album
registerProperty(new PropertyMetaInformation(Const.XML_ALBUM, false, true, true, true, true,
String.class, null));
// Genre
registerProperty(new PropertyMetaInformation(Const.XML_GENRE, false, true, true, true, true,
String.class, null));
// Artist
registerProperty(new PropertyMetaInformation(Const.XML_ARTIST, false, true, true, true, true,
String.class, null));
// Album-artist
registerProperty(new PropertyMetaInformation(Const.XML_ALBUM_ARTIST, false, false, true, true,
true, String.class, null));
// Length
registerProperty(new PropertyMetaInformation(Const.XML_TRACK_LENGTH, false, true, true, false,
false, Long.class, null));
// Type
registerProperty(new PropertyMetaInformation(Const.XML_TRACK_TYPE, false, true, true, false,
false, Long.class, null));
// Year
registerProperty(new PropertyMetaInformation(Const.XML_YEAR, false, true, true, true, true,
Long.class, 0));
// Rate : this is a property computed from preference and total played time,
// not editable
registerProperty(new PropertyMetaInformation(Const.XML_TRACK_RATE, false, false, true, false,
true, Long.class, 0));
// Files. This is now a derivated property, build on demand. Never commited because always null.
registerProperty(new PropertyMetaInformation(Const.XML_FILES, false, false, true, false, false,
String.class, null));
// Hits
registerProperty(new PropertyMetaInformation(Const.XML_TRACK_HITS, false, false, true, false,
false, Long.class, 0));
// Addition date
registerProperty(new PropertyMetaInformation(Const.XML_TRACK_DISCOVERY_DATE, false, false,
true, false, true, Date.class, null));
// Comment
registerProperty(new PropertyMetaInformation(Const.XML_TRACK_COMMENT, false, false, true, true,
true, String.class, null));
// Track order
registerProperty(new PropertyMetaInformation(Const.XML_TRACK_ORDER, false, true, true, true,
false, Long.class, null));
// Track disc number
registerProperty(new PropertyMetaInformation(Const.XML_TRACK_DISC_NUMBER, false, true, true,
true, true, Long.class, null));
// Track preference factor. This is not editable because when changing
// preference, others
// actions must be done (updateRate() and we want user to use contextual
// menus and commands instead of the properties wizard to set preference)
registerProperty(new PropertyMetaInformation(Const.XML_TRACK_PREFERENCE, false, false, true,
false, true, Long.class, 0l));
// Track total playtime
registerProperty(new PropertyMetaInformation(Const.XML_TRACK_TOTAL_PLAYTIME, false, false,
true, false, false, Long.class, 0l));
// Track ban status
registerProperty(new PropertyMetaInformation(Const.XML_TRACK_BANNED, false, false, true, true,
false, Boolean.class, false));
// Scrobble flag
registerProperty(new PropertyMetaInformation(Const.XML_TRACK_SCROBBLE, false, false, true,
true, true, Boolean.class, true));
}
/**
* Gets the instance.
*
* @return singleton
*/
public static TrackManager getInstance() {
return singleton;
}
/**
* Confirm before actually changing a tag
* @param track the track to be changed
* @return whether user accept to actually change a tag
*/
private boolean confirm(Track track) {
if (Conf.getBoolean(Const.CONF_CONFIRMATIONS_BEFORE_TAG_WRITE)) {
final int iResu = Messages.getChoice(Messages.getString("Confirmation_tag_write") + " : \n"
+ track.getFilesString(), JOptionPane.YES_NO_CANCEL_OPTION,
JOptionPane.INFORMATION_MESSAGE);
return iResu == JOptionPane.YES_OPTION;
}
return true;
}
/**
* Register an Track.
*
* @param sName
* @param album
* @param genre
* @param artist
* @param length
* @param year
* @param lOrder
* @param type
* @param lDiscNumber
*
* @return the track
*/
public Track registerTrack(String sName, Album album, Genre genre, Artist artist, long length,
Year year, long lOrder, Type type, long lDiscNumber) {
String sId = createID(sName, album, genre, artist, length, year, lOrder, type, lDiscNumber);
return registerTrack(sId, sName, album, genre, artist, length, year, lOrder, type, lDiscNumber);
}
/**
* Return hashcode for a track.
*
* @param sName
* @param album
* @param genre
* @param artist
* @param length
* @param year
* @param lOrder
* @param type
* @param lDiscNumber
*
* @return the string
*/
protected static String createID(String sName, Album album, Genre genre, Artist artist,
long length, Year year, long lOrder, Type type, long lDiscNumber) {
StringBuilder sb = new StringBuilder(100);
sb.append(genre.getID()).append(artist.getID()).append(album.getID()).append(sName)
.append(year.getValue()).append(length).append(lOrder).append(type.getID())
.append(lDiscNumber);
// distinguish tracks by type because we can't find best file
// on different quality levels by format
return MD5Processor.hash(sb.toString());
}
/**
* Register an Track with a known id.
*
* @param sId
* @param sName
* @param album
* @param genre
* @param artist
* @param length
* @param year
* @param lOrder
* @param type
* @param lDiscNumber
* @return the track
*/
public Track registerTrack(String sId, String sName, Album album, Genre genre, Artist artist,
long length, Year year, long lOrder, Type type, long lDiscNumber) {
lock.writeLock().lock();
try {
// We absolutely need to return the same track if already registrated to
// avoid duplicates and properties lost
Track track = getTrackByID(sId);
if (track != null) {
return track;
}
track = new Track(sId, sName, album, genre, artist, length, year, lOrder, type, lDiscNumber);
registerItem(track);
// For performances, add the track to the album cache
List<Track> cache = track.getAlbum().getTracksCache();
synchronized (cache) {
cache.add(track);
}
return track;
} finally {
lock.writeLock().unlock();
}
}
/**
* Commit tags.
*
* @throws JajukException the jajuk exception
*
* @throw an exception if a tag cannot be commited
*/
public void commit() throws JajukException {
try {
lock.writeLock().lock();
// Iterate over a defensive copy to avoid concurrent issues (note also that
// several threads can commit at the same time). We synchronize the copy and
// we drop tags to commit.
List<Tag> toCommit = null;
synchronized (tagsToCommit) {
toCommit = new ArrayList<Tag>(tagsToCommit);
tagsToCommit.clear();
}
for (Tag tag : toCommit) {
try {
tag.commit();
} catch (Exception e) {
Log.error(e);
try {
// If actual tag commit fails, we have to undo changes made in memory for current track
// The best solution for this complex issue is to force a deep refresh of the directory
// and to clear the tag cache to force tags reload
Tag.clearCache();
Directory dir = null;
File file = FileManager.getInstance().getFileByPath(tag.getFio().getAbsolutePath());
if (file != null) {
dir = file.getDirectory();
}
dir.refresh(true);
// refresh views
ObservationManager.notify(new JajukEvent(JajukEvents.DEVICE_REFRESH));
} catch (Exception e2) {
Log.error(e2);
}
throw new JajukException(104, e);
}
}
// Clear the tag cache after a transaction to
// avoid memory leaks
Tag.clearCache();
} finally {
lock.writeLock().unlock();
}
}
/**
* Change a track album.
*
* @param track
* @param sNewAlbum
* @param filter files we want to deal with
* @return new track
* @throws JajukException the jajuk exception
*/
public Track changeTrackAlbum(Track track, String sNewAlbum, Set<File> filter)
throws JajukException {
try {
lock.writeLock().lock();
if (!confirm(track)) {
return track;
}
// check there is actually a change
if (track.getAlbum().getName2().equals(sNewAlbum)) {
return track;
}
List<File> alReady = null;
// check if files are accessible
alReady = track.getReadyFiles(filter);
if (alReady.size() == 0) {
throw new NoneAccessibleFileException(10);
}
// change tag in files
for (File file : alReady) {
Tag tag = Tag.getTagForFio(file.getFIO(), false);
tag.setAlbumName(sNewAlbum);
if (bAutocommit) {
tag.commit();
} else {
tagsToCommit.add(tag);
}
}
// Remove the track from the old album
List<Track> cache = track.getAlbum().getTracksCache();
synchronized (cache) {
cache.remove(track);
}
// if current track album name is changed, notify it
if (QueueModel.getPlayingFile() != null
&& QueueModel.getPlayingFile().getTrack().getAlbum().equals(track.getAlbum())) {
ObservationManager.notify(new JajukEvent(JajukEvents.ALBUM_CHANGED));
}
// register the new album
Album newAlbum = AlbumManager.getInstance().registerAlbum(sNewAlbum,
track.getAlbum().getDiscID());
Track newTrack = registerTrack(track.getName(), newAlbum, track.getGenre(),
track.getArtist(), track.getDuration(), track.getYear(), track.getOrder(),
track.getType(), track.getDiscNumber());
postChange(track, newTrack, filter);
// remove this album if no more references
AlbumManager.getInstance().cleanOrphanTracks(track.getAlbum());
return newTrack;
} finally {
lock.writeLock().unlock();
}
}
/**
* Change a track artist.
*
* @param track
* @param sNewArtist
* @param filter files we want to deal with
* @return new track
* @throws JajukException the jajuk exception
*/
public Track changeTrackArtist(Track track, String sNewArtist, Set<File> filter)
throws JajukException {
try {
lock.writeLock().lock();
if (!confirm(track)) {
return track;
}
// check there is actually a change
if (track.getArtist().getName2().equals(sNewArtist)) {
return track;
}
List<File> alReady = null;
// check if files are accessible
alReady = track.getReadyFiles(filter);
if (alReady.size() == 0) {
throw new NoneAccessibleFileException(10);
}
// change tag in files
for (final File file : alReady) {
final Tag tag = Tag.getTagForFio(file.getFIO(), false);
tag.setArtistName(sNewArtist);
if (bAutocommit) {
tag.commit();
} else {
tagsToCommit.add(tag);
}
}
// Remove the track from the old album
List<Track> cache = track.getAlbum().getTracksCache();
synchronized (cache) {
cache.remove(track);
}
// if current track artist name is changed, notify it
if (QueueModel.getPlayingFile() != null
&& QueueModel.getPlayingFile().getTrack().getArtist().equals(track.getArtist())) {
ObservationManager.notify(new JajukEvent(JajukEvents.ARTIST_CHANGED));
}
// register the new item
Artist newArtist = ArtistManager.getInstance().registerArtist(sNewArtist);
Track newTrack = registerTrack(track.getName(), track.getAlbum(), track.getGenre(),
newArtist, track.getDuration(), track.getYear(), track.getOrder(), track.getType(),
track.getDiscNumber());
postChange(track, newTrack, filter);
// remove this item if no more references
ArtistManager.getInstance().cleanOrphanTracks(track.getArtist());
return newTrack;
} finally {
lock.writeLock().unlock();
}
}
/**
* Change a track genre.
*
* @param track
* @param sNewGenre
* @param filter files we want to deal with
* @return new track
* @throws JajukException the jajuk exception
*/
public Track changeTrackGenre(Track track, String sNewGenre, Set<File> filter)
throws JajukException {
try {
lock.writeLock().lock();
if (!confirm(track)) {
return track;
}
// check there is actually a change
if (track.getGenre().getName2().equals(sNewGenre)) {
return track;
}
List<File> alReady = null;
// check if files are accessible
alReady = track.getReadyFiles(filter);
if (alReady.size() == 0) {
throw new NoneAccessibleFileException(10);
}
// change tag in files
for (final File file : alReady) {
Tag tag = Tag.getTagForFio(file.getFIO(), false);
tag.setGenreName(sNewGenre);
if (bAutocommit) {
tag.commit();
} else {
tagsToCommit.add(tag);
}
}
// Remove the track from the old album
List<Track> cache = track.getAlbum().getTracksCache();
synchronized (cache) {
cache.remove(track);
}
// register the new item
Genre newGenre = GenreManager.getInstance().registerGenre(sNewGenre);
Track newTrack = registerTrack(track.getName(), track.getAlbum(), newGenre,
track.getArtist(), track.getDuration(), track.getYear(), track.getOrder(),
track.getType(), track.getDiscNumber());
postChange(track, newTrack, filter);
// remove this item if no more references
GenreManager.getInstance().cleanOrphanTracks(track.getGenre());
return newTrack;
} finally {
lock.writeLock().unlock();
}
}
/**
* Change a track year.
*
* @param track
* @param newItem
* @param filter files we want to deal with
* @return new track or null if wrong format
* @throws JajukException the jajuk exception
*/
public Track changeTrackYear(Track track, String newItem, Set<File> filter) throws JajukException {
try {
lock.writeLock().lock();
if (!confirm(track)) {
return track;
}
// check there is actually a change
if (track.getYear().getName().equals(newItem)) {
return track;
}
long lNewItem = UtilString.fastLongParser(newItem);
if (lNewItem < 0 || lNewItem > 10000) {
throw new JajukException(137);
}
List<File> alReady = null;
// check if files are accessible
alReady = track.getReadyFiles(filter);
if (alReady.size() == 0) {
throw new NoneAccessibleFileException(10);
}
// change tag in files
for (final File file : alReady) {
Tag tag = Tag.getTagForFio(file.getFIO(), false);
tag.setYear(newItem);
if (bAutocommit) {
tag.commit();
} else {
tagsToCommit.add(tag);
}
}
// Remove the track from the old album
List<Track> cache = track.getAlbum().getTracksCache();
synchronized (cache) {
cache.remove(track);
}
// Register new item
Year newYear = YearManager.getInstance().registerYear(newItem);
Track newTrack = registerTrack(track.getName(), track.getAlbum(), track.getGenre(),
track.getArtist(), track.getDuration(), newYear, track.getOrder(), track.getType(),
track.getDiscNumber());
postChange(track, newTrack, filter);
return newTrack;
} finally {
lock.writeLock().unlock();
}
}
/**
* Change a track comment.
*
* @param track
* @param sNewItem
* @param filter files we want to deal with
* @return new track or null if wrong format
* @throws JajukException the jajuk exception
*/
Track changeTrackComment(Track track, String sNewItem, Set<File> filter) throws JajukException {
try {
lock.writeLock().lock();
if (!confirm(track)) {
return track;
}
// check there is actually a change
if (track.getComment().equals(sNewItem)) {
return track;
}
List<File> alReady = null;
// check if files are accessible
alReady = track.getReadyFiles(filter);
if (alReady.size() == 0) {
throw new NoneAccessibleFileException(10);
}
// change tag in files
for (File file : alReady) {
Tag tag = Tag.getTagForFio(file.getFIO(), false);
tag.setComment(sNewItem);
if (bAutocommit) {
tag.commit();
// Force files resorting to ensure the sorting consistency
// Do it here only because the sorting is a long operation already done
// by the TrackManager.commit() method caller (PropertiesDialog for ie).
// When called for a table change for ie, the sorting must be done for
// each change.
FileManager.getInstance().forceSorting();
} else {
tagsToCommit.add(tag);
}
}
track.setComment(sNewItem);
return track;
} finally {
lock.writeLock().unlock();
}
}
/**
* Change a track rate.
*
* @param track
* @param lNew
*
* @return new track or null if wrong format
*/
public Track changeTrackRate(Track track, long lNew) {
try {
lock.writeLock().lock();
// No confirmation here as this code is called during startup and is not available from GUI anyway
// check there is actually a change
if (track.getRate() == lNew) {
return track;
}
// check format, rate in [0,100]
if (lNew < 0 || lNew > 100) {
track.setRate(0l);
Log.error(137);
} else {
track.setRate(lNew);
}
return track;
} finally {
lock.writeLock().unlock();
}
}
/**
* Change a track order.
*
* @param track
* @param lNewOrder
* @param filter files we want to deal with
* @return new track or null if wrong format
* @throws JajukException the jajuk exception
*/
public Track changeTrackOrder(Track track, long lNewOrder, Set<File> filter)
throws JajukException {
try {
lock.writeLock().lock();
if (!confirm(track)) {
return track;
}
// check there is actually a change
if (track.getOrder() == lNewOrder) {
return track;
}
// check format
if (lNewOrder < 0) {
throw new JajukException(137);
}
List<File> alReady = null;
// check if files are accessible
alReady = track.getReadyFiles(filter);
if (alReady.size() == 0) {
throw new NoneAccessibleFileException(10);
}
// change tag in files
for (File file : alReady) {
Tag tag = Tag.getTagForFio(file.getFIO(), false);
tag.setOrder(lNewOrder);
if (bAutocommit) {
tag.commit();
} else {
tagsToCommit.add(tag);
}
}
// Remove the track from the old album
List<Track> cache = track.getAlbum().getTracksCache();
synchronized (cache) {
cache.remove(track);
}
Track newTrack = registerTrack(track.getName(), track.getAlbum(), track.getGenre(),
track.getArtist(), track.getDuration(), track.getYear(), lNewOrder, track.getType(),
track.getDiscNumber());
postChange(track, newTrack, filter);
return newTrack;
} finally {
lock.writeLock().unlock();
}
}
/**
* Change a generic field.
*
* @param track the track to write against.
* @param tagFieldKey the tag key
* @param tagFieldValue the tag value as a string
* @param filter files we want to deal with
*
* @return the track
*
* @throws JajukException if the tag can't be written
*/
public Track changeTrackField(Track track, String tagFieldKey, String tagFieldValue,
Set<File> filter) throws JajukException {
try {
lock.writeLock().lock();
if (!confirm(track)) {
return track;
}
List<File> alReady = null;
// check if files are accessible
alReady = track.getReadyFiles();
if (alReady.size() == 0) {
throw new NoneAccessibleFileException(10);
}
// change tag in files
for (File file : alReady) {
Tag tag = Tag.getTagForFio(file.getFIO(), false);
tag.setTagField(tagFieldKey, tagFieldValue);
if (bAutocommit) {
tag.commit();
} else {
tagsToCommit.add(tag);
}
}
return track;
} finally {
lock.writeLock().unlock();
}
}
/**
* Change a track name.
*
* @param track
* @param sNewItem
* @param filter files we want to deal with
* @return new track
* @throws JajukException the jajuk exception
*/
public Track changeTrackName(Track track, String sNewItem, Set<File> filter)
throws JajukException {
try {
lock.writeLock().lock();
if (!confirm(track)) {
return track;
}
// check there is actually a change
if (track.getName().equals(sNewItem)) {
return track;
}
List<File> alReady = null;
// check if files are accessible
alReady = track.getReadyFiles(filter);
if (alReady.size() == 0) {
throw new NoneAccessibleFileException(10);
}
// change tag in files
for (File file : alReady) {
Tag tag = Tag.getTagForFio(file.getFIO(), false);
tag.setTrackName(sNewItem);
if (bAutocommit) {
tag.commit();
} else {
tagsToCommit.add(tag);
}
}
// Remove old track from the album
List<Track> cache = track.getAlbum().getTracksCache();
synchronized (cache) {
cache.remove(track);
}
Track newTrack = registerTrack(sNewItem, track.getAlbum(), track.getGenre(),
track.getArtist(), track.getDuration(), track.getYear(), track.getOrder(),
track.getType(), track.getDiscNumber());
postChange(track, newTrack, filter);
// if current track name is changed, notify it
if (QueueModel.getPlayingFile() != null
&& QueueModel.getPlayingFile().getTrack().equals(track)) {
ObservationManager.notify(new JajukEvent(JajukEvents.TRACK_CHANGED));
}
return newTrack;
} finally {
lock.writeLock().unlock();
}
}
/**
* Change track album artist.
*
* @param track
* @param sNewItem
* @param filter
* @return the item
* @throws JajukException the jajuk exception
*/
Item changeTrackAlbumArtist(Track track, String sNewItem, Set<File> filter) throws JajukException {
try {
lock.writeLock().lock();
if (!confirm(track)) {
return track;
}
// check there is actually a change
if (track.getAlbumArtist() != null && track.getAlbumArtist().getName2().equals(sNewItem)) {
return track;
}
List<File> alReady = null;
// check if files are accessible
alReady = track.getReadyFiles(filter);
if (alReady.size() == 0) {
throw new NoneAccessibleFileException(10);
}
// change tag in files
for (File file : alReady) {
Tag tag = Tag.getTagForFio(file.getFIO(), false);
tag.setAlbumArtist(sNewItem);
if (bAutocommit) {
tag.commit();
// Force files resorting to ensure the sorting consistency
// Do it here only because the sorting is a long operation already done
// by the TrackManager.commit() method caller (PropertiesDialog for ie).
// When called for a table change for ie, the sorting must be done for
// each change.
FileManager.getInstance().forceSorting();
} else {
tagsToCommit.add(tag);
}
}
// register the new item
AlbumArtist newAlbumArtist = AlbumArtistManager.getInstance().registerAlbumArtist(sNewItem);
track.setAlbumArtist(newAlbumArtist);
return track;
} finally {
lock.writeLock().unlock();
}
}
/**
* Change track disc number.
*
* @param track
* @param lNewDiscNumber
* @param filter
* @return the item
* @throws JajukException the jajuk exception
*/
public Item changeTrackDiscNumber(Track track, long lNewDiscNumber, Set<File> filter)
throws JajukException {
try {
lock.writeLock().lock();
if (!confirm(track)) {
return track;
}
// check there is actually a change
if (track.getDiscNumber() == lNewDiscNumber) {
return track;
}
// check format
if (lNewDiscNumber < 0) {
throw new JajukException(137);
}
List<File> alReady = null;
// check if files are accessible
alReady = track.getReadyFiles(filter);
if (alReady.size() == 0) {
throw new NoneAccessibleFileException(10);
}
// change tag in files
for (File file : alReady) {
Tag tag = Tag.getTagForFio(file.getFIO(), false);
tag.setDiscNumber(lNewDiscNumber);
if (bAutocommit) {
tag.commit();
} else {
tagsToCommit.add(tag);
}
}
// Remove the track from the old album
List<Track> cache = track.getAlbum().getTracksCache();
synchronized (cache) {
cache.remove(track);
}
// if current track album name is changed, notify it
if (QueueModel.getPlayingFile() != null
&& QueueModel.getPlayingFile().getTrack().getAlbum().equals(track.getAlbum())) {
ObservationManager.notify(new JajukEvent(JajukEvents.ALBUM_CHANGED));
}
Track newTrack = registerTrack(track.getName(), track.getAlbum(), track.getGenre(),
track.getArtist(), track.getDuration(), track.getYear(), track.getDiscNumber(),
track.getType(), lNewDiscNumber);
postChange(track, newTrack, filter);
return newTrack;
} finally {
lock.writeLock().unlock();
}
}
/**
* Update files references.
*
* @param oldTrack
* @param newTrack
* @param filter
*/
private void updateFilesReferences(Track oldTrack, Track newTrack, Set<File> filter) {
lock.writeLock().lock();
try {
// Reset files property before adding new files
for (File file : oldTrack.getReadyFiles(filter)) {
file.setTrack(newTrack);// set new track for the changed file
}
} finally {
lock.writeLock().unlock();
}
}
/**
* Post change.
*
* @param track
* @param newTrack
* @param filter
*/
private void postChange(Track track, Track newTrack, Set<File> filter) {
lock.writeLock().lock();
try {
// re apply old properties from old item
newTrack.cloneProperties(track);
// update files references
updateFilesReferences(track, newTrack, filter);
if (track.getFiles().size() == 0) { // normal case: old track has no
// more associated
// tracks, remove it
removeItem(track);// remove old track
}
} finally {
lock.writeLock().unlock();
}
}
/**
* Perform a track cleanup : delete useless items.
*/
@Override
public void cleanup() {
// No need to lock or synchronize, getTracks() is a defensive copy of tracks
for (Track track : getTracks()) {
if (track.getFiles().size() == 0) { // no associated file
removeItem(track);
continue;
}
// Cleanup all files no more attached to a track
for (File file : track.getFiles()) {
if (FileManager.getInstance().getFileByID(file.getID()) == null) {
FileManager.getInstance().removeFile(file);
}
}
if (track.getFiles().size() == 0) { // the track don't map
// anymore to any physical item, just remove it
removeItem(track);
}
}
}
/**
* Remove a file mapping from a track.
*
* @param file
*/
public void removeFile(File file) {
Track track = file.getTrack();
lock.writeLock().lock();
try {
// Remove file reference
track.removeFile(file);
// Put it in the attic
attic.put(track.getID(), track);
// If the track contained a single file, drop it
if (track.getFiles().size() == 0) {
// the track don't map
// anymore to any physical item, just remove it
removeItem(track);
}
} finally {
lock.writeLock().unlock();
}
}
/**
* Return a track from attic or null if not found
* @param id the track id to search for
* @return a track from attic or null if not found
*/
public Track getTrackFromAttic(String id) {
return attic.get(id);
}
/*
* (non-Javadoc)
*
* @see org.jajuk.base.ItemManager#getIdentifier()
*/
@Override
public String getXMLTag() {
return Const.XML_TRACKS;
}
/**
* Get ordered tracks list associated with this item
* <p>
* This is a defensive copy only
* </p>
* .
*
* @param item
* @param sorted Whether the output should be sorted on it (actually applied on
* artists,years and genres because others items are already sorted)
*
* @return the associated tracks
*/
public List<Track> getAssociatedTracks(Item item, boolean sorted) {
List<Item> items = new ArrayList<Item>(1);
items.add(item);
return getAssociatedTracks(items, sorted);
}
/**
* Get ordered tracks list associated with a list of items (of the same type)
* <p>
* This is a defensive copy only
* </p>
* .
*
* @param items
* @param sorted Whether the output should be sorted on it (actually applied on
* artists,years and genres because others items are already sorted)
* @return the associated tracks
*/
@SuppressWarnings("unchecked")
public List<Track> getAssociatedTracks(List<Item> items, boolean sorted) {
if (items == null || items.size() == 0) {
return new ArrayList<Track>(0);
}
List<Track> out = new ArrayList<Track>(items.size());
if (items.get(0) instanceof Album) {
// check the album cache
for (Item item : items) {
List<Track> tracks = ((Album) item).getTracksCache();
synchronized (tracks) {
if (tracks.size() > 0) {
out.addAll(tracks);
}
}
}
// cache is not sorted correct for albums with more than 1 disc
if (sorted) {
// sort Tracks
Collections.sort(out, new TrackComparator(TrackComparatorType.ORDER));
}
}
// If the item is itself a track, simply return it
else if (items.get(0) instanceof Track) {
for (Item item : items) {
out.add((Track) item);
}
if (sorted) {
Collections.sort(out, new TrackComparator(TrackComparatorType.ALBUM));
}
} else if (items.get(0) instanceof File) {
for (Item item : items) {
out.add(((File) item).getTrack());
}
if (sorted) {
Collections.sort(out, new TrackComparator(TrackComparatorType.ALBUM));
}
} else if (items.get(0) instanceof Directory) {
for (Item item : items) {
Directory dir = (Directory) item;
for (File file : dir.getFilesRecursively()) {
Track track = file.getTrack();
// Caution, do not add dups
if (!out.contains(track)) {
out.add(file.getTrack());
}
}
}
if (sorted) {
Collections.sort(out, new TrackComparator(TrackComparatorType.ORDER));
}
} else if (items.get(0) instanceof Playlist) {
for (Item item : items) {
Playlist pl = (Playlist) item;
List<File> files;
try {
files = pl.getFiles();
} catch (JajukException e) {
Log.warn("Cannot parse playlist : " + pl.getAbsolutePath());
return out;
}
for (File file : files) {
Track track = file.getTrack();
// Caution, do not add dups
if (!out.contains(track)) {
out.add(file.getTrack());
}
}
}
if (sorted) {
Collections.sort(out, new TrackComparator(TrackComparatorType.ALBUM));
}
} else if (items.get(0) instanceof Artist) {
Iterator<Item> tracks = (Iterator<Item>) getItemsIterator();
while (tracks.hasNext()) {
Track track = (Track) tracks.next();
if (items.contains(track.getArtist())) {
out.add(track);
}
// Sort by album
if (sorted) {
Collections.sort(out, new TrackComparator(TrackComparatorType.ARTIST_ALBUM));
}
}
return out;
} else if (items.get(0) instanceof Genre) {
Iterator<Item> tracks = (Iterator<Item>) getItemsIterator();
while (tracks.hasNext()) {
Track track = (Track) tracks.next();
if (items.contains(track.getGenre())) {
out.add(track);
}
// Sort by genre
if (sorted) {
Collections.sort(out, new TrackComparator(TrackComparatorType.GENRE_ARTIST_ALBUM));
}
}
} else if (items.get(0) instanceof Year) {
Iterator<Item> tracks = (Iterator<Item>) getItemsIterator();
while (tracks.hasNext()) {
Track track = (Track) tracks.next();
if (items.contains(track.getYear())) {
out.add(track);
}
// Sort by year
if (sorted) {
Collections.sort(out, new TrackComparator(TrackComparatorType.YEAR_ALBUM));
}
}
}
return out;
}
/**
* Gets the comparator.
*
* @return the comparator
*/
public TrackComparator getComparator() {
return new TrackComparator(
TrackComparatorType.values()[Conf.getInt(Const.CONF_LOGICAL_TREE_SORT_ORDER)]);
}
/**
* Gets the track by id.
*
* @param sID Item ID
*
* @return item
*/
public Track getTrackByID(String sID) {
return (Track) getItemByID(sID);
}
/**
* Gets the tracks.
*
* @return ordered tracks list
*/
@SuppressWarnings("unchecked")
public List<Track> getTracks() {
return (List<Track>) getItems();
}
/**
* Gets the tracks iterator.
*
* @return tracks iterator
*/
@SuppressWarnings("unchecked")
public ReadOnlyIterator<Track> getTracksIterator() {
return new ReadOnlyIterator<Track>((Iterator<Track>) getItemsIterator());
}
/**
* Perform a search in all files names with given criteria.
*
* @param criteria
*
* @return an ordered list of available files
*/
public List<SearchResult> search(String criteria) {
lock.readLock().lock();
try {
boolean hide = Conf.getBoolean(Const.CONF_OPTIONS_HIDE_UNMOUNTED);
List<SearchResult> resu = new ArrayList<SearchResult>();
ReadOnlyIterator<Track> tracks = getTracksIterator();
while (tracks.hasNext()) {
Track track = tracks.next();
File playable = track.getBestFile(hide);
if (playable != null) {
String sResu = track.getAny();
if (sResu.toLowerCase(Locale.getDefault()).indexOf(
criteria.toLowerCase(Locale.getDefault())) != -1) {
resu.add(new SearchResult(playable, playable.toStringSearch()));
}
}
}
return resu;
} finally {
lock.readLock().unlock();
}
}
/**
* Checks if is autocommit.
*
* @return autocommit behavior for tags
*/
public boolean isAutocommit() {
return this.bAutocommit;
}
/**
* Set autocommit behavior for tags.
*
* @param autocommit should tag changes be commited at each change or on demand ?
*/
public void setAutocommit(boolean autocommit) {
this.bAutocommit = autocommit;
}
}