/*
* Digital Audio Access Protocol (DAAP) Library
* Copyright (C) 2004-2010 Roger Kapsi
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.ardverk.daap;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import org.ardverk.daap.chunks.BooleanChunk;
import org.ardverk.daap.chunks.Chunk;
import org.ardverk.daap.chunks.StringChunk;
import org.ardverk.daap.chunks.UByteChunk;
import org.ardverk.daap.chunks.impl.DeletedIdListing;
import org.ardverk.daap.chunks.impl.ItemCount;
import org.ardverk.daap.chunks.impl.ItemId;
import org.ardverk.daap.chunks.impl.ItemName;
import org.ardverk.daap.chunks.impl.Listing;
import org.ardverk.daap.chunks.impl.ListingItem;
import org.ardverk.daap.chunks.impl.PersistentId;
import org.ardverk.daap.chunks.impl.PlaylistRepeatMode;
import org.ardverk.daap.chunks.impl.PlaylistShuffleMode;
import org.ardverk.daap.chunks.impl.PlaylistSongs;
import org.ardverk.daap.chunks.impl.PodcastPlaylist;
import org.ardverk.daap.chunks.impl.ReturnedCount;
import org.ardverk.daap.chunks.impl.SmartPlaylist;
import org.ardverk.daap.chunks.impl.SpecifiedTotalCount;
import org.ardverk.daap.chunks.impl.Status;
import org.ardverk.daap.chunks.impl.UpdateType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The name is self explaining. A Playlist is a set of Songs.
*
* @author Roger Kapsi
*/
public class Playlist {
private static final Logger LOG = LoggerFactory.getLogger(Playlist.class);
/** playlistId is an 32bit unsigned value! */
private static final AtomicLong PLAYLIST_ID = new AtomicLong(1);
/** unique id */
private final ItemId itemId = new ItemId(PLAYLIST_ID.getAndIncrement());
/** unique persistent id */
private final PersistentId persistentId = new PersistentId();
/** The name of playlist */
private final ItemName itemName = new ItemName();
/** the total number of songs in this playlist */
private final ItemCount itemCount = new ItemCount();
private SmartPlaylist smartPlaylist;
// @since iTunes 5.0
private PlaylistRepeatMode repeatMode;
private PlaylistShuffleMode shuffleMode;
private PodcastPlaylist podcastPlaylist;
// private HasChildContainers hasChildContainers;
/** */
private final Map<String, Chunk> chunks = new HashMap<String, Chunk>();
/** Set of Songs */
private final List<Song> songs = new ArrayList<Song>();
/** Set of deleted Songs */
private Set<Song> deletedSongs = null;
protected Playlist(Playlist playlist, Transaction txn) {
this.itemId.setValue(playlist.itemId.getValue());
this.itemName.setValue(playlist.itemName.getValue());
this.persistentId.setValue(playlist.persistentId.getValue());
this.itemCount.setValue(playlist.itemCount.getValue());
if (playlist.deletedSongs != null) {
this.deletedSongs = playlist.deletedSongs;
playlist.deletedSongs = null;
}
for (Song song : playlist.songs) {
if (txn.modified(song)) {
if (deletedSongs == null || !deletedSongs.contains(song)) {
// cloning is not necessary
// songs.add(new Song(song));
songs.add(song);
}
}
}
init();
}
/**
* Creates a new Playlist
*
* @param name
* the Name of the Playlist
*/
public Playlist(String name) {
this.itemName.setValue(name);
this.persistentId.setValue(Library.nextPersistentId());
init();
}
private void init() {
addChunk(itemId);
addChunk(itemName);
addChunk(persistentId);
addChunk(itemCount);
}
/**
* Returns the unique ID of this Playlist
*
* @return unique ID of this Playlist
*/
protected long getItemId() {
return itemId.getUnsignedValue();
}
/**
*
* @param chunk
*/
protected void addChunk(Chunk chunk) {
chunks.put(chunk.getName(), chunk);
}
protected void removeChunk(Chunk chunk) {
chunks.remove(chunk.getName());
}
protected Chunk getChunk(String name) {
return chunks.get(name);
}
/**
* Sets the name of this Playlist
*
* @param txn
* a Transaction
* @param name
* a new name
* @throws DaapException
*/
public void setName(Transaction txn, String name) {
if (txn != null) {
txn.addTxn(this, createNewTxn("itemName", name));
} else {
setValue("itemName", name);
}
}
/**
* Returns the name of this Playlist
*
* @return the name of this Playlist
*/
public String getName() {
return getStringValue(itemName);
}
/**
* Sets whether or not this Playlist is a smart playlist. The difference
* between smart and common playlists is that smart playlists have a star as
* an icon and they appear as first in the list.
*/
public void setSmartPlaylist(Transaction txn, boolean smartPlaylist) {
if (txn != null) {
txn.addTxn(this, createNewTxn("smartPlaylist", smartPlaylist));
} else {
setValue("smartPlaylist", smartPlaylist);
}
}
/**
* Returns <code>true</code> if this Playlist is a smart playlist.
*/
public boolean isSmartPlaylist() {
return getBooleanValue(smartPlaylist);
}
/**
*
*/
public void setPodcastPlaylist(Transaction txn,
final boolean podcastPlaylist) {
if (txn != null) {
txn.addTxn(this, createNewTxn("podcastPlaylist", podcastPlaylist));
} else {
setValue("podcastPlaylist", podcastPlaylist);
}
}
/**
*
*/
public boolean isPodcastPlaylist() {
return getBooleanValue(podcastPlaylist);
}
/**
*
*/
public void setRepeatMode(Transaction txn, int repeatMode) {
UByteChunk.checkUByteRange(repeatMode);
if (txn != null) {
txn.addTxn(this, createNewTxn("repeatMode", repeatMode));
} else {
setValue("repeatMode", repeatMode);
}
}
/**
*
*/
public int getRepeatMode() {
return getUByteValue(repeatMode);
}
/**
*
*/
public void setShuffleMode(Transaction txn, int shuffleMode) {
UByteChunk.checkUByteRange(shuffleMode);
if (txn != null) {
txn.addTxn(this, createNewTxn("shuffleMode", shuffleMode));
} else {
setValue("shuffleMode", shuffleMode);
}
}
/**
*
*/
public int getShuffleMode() {
return getUByteValue(shuffleMode);
}
/**
* Returns the number of Songs in this Playlist
*/
public int getSongCount() {
return songs.size();
}
/**
* Retuns an unmodifiable set of all songs
*/
public List<Song> getSongs() {
return Collections.unmodifiableList(songs);
}
/**
* Returns a set of deleted Songs or null if no Songs were removed from this
* Playlist
*/
protected Set<Song> getDeletedSongs() {
return deletedSongs;
}
/**
* Adds <code>song</code> to this Playlist
*
* @param song
* @throws DaapTransactionException
*/
public void addSong(Transaction txn, final Song song) {
if (txn != null) {
txn.addTxn(this, new Txn() {
public void commit(Transaction txn) {
addSongP(txn, song);
}
});
txn.attach(song); //
} else {
addSongP(txn, song);
}
}
private void addSongP(Transaction txn, Song song) {
if (!containsSong(song) && songs.add(song)) {
itemCount.setValue(songs.size());
if (deletedSongs != null && deletedSongs.remove(song)
&& deletedSongs.isEmpty()) {
deletedSongs = null;
}
}
}
/**
* Removes <code>song</code> from this Playlist
*
* @param song
* @throws DaapTransactionException
*/
public void removeSong(Transaction txn, final Song song) {
if (txn != null) {
txn.addTxn(this, new Txn() {
public void commit(Transaction txn) {
removeSongP(txn, song);
}
});
} else {
removeSongP(txn, song);
}
}
private void removeSongP(Transaction txn, Song song) {
if (songs.remove(song)) {
itemCount.setValue(songs.size());
if (deletedSongs == null) {
deletedSongs = new HashSet<Song>();
}
deletedSongs.add(song);
}
}
/**
* Gets and returns a Song by its ID
*
* @param songId
* @return
*/
protected Song getSong(DaapRequest request) {
long songId = request.getItemId();
for (Song song : songs) {
if (song.getItemId() == songId) {
return song;
}
}
return null;
}
/**
* Performs a select on this Playlist and returns something for the request
* or <code>null</code>
*
* @param request
* a DaapRequest
* @return a response for the DaapRequest
*/
protected Object select(DaapRequest request) {
if (request.isPlaylistSongsRequest()) {
return getPlaylistSongs(request);
} else if (LOG.isInfoEnabled()) {
LOG.info("Unknown request: " + request);
}
return null;
}
/**
* Returns <code>true</code> if the provided <code>song</code> is in this
* Playlist.
*/
public boolean containsSong(Song song) {
return songs.contains(song);
}
@Override
public int hashCode() {
return itemId.getValue();
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Playlist)) {
return false;
}
return ((Playlist) o).getItemId() == getItemId();
}
@Override
public String toString() {
return getName() + " (" + getItemId() + ")";
}
private PlaylistSongs getPlaylistSongs(DaapRequest request) {
PlaylistSongs playlistSongs = new PlaylistSongs();
playlistSongs.add(new Status(200));
playlistSongs.add(new UpdateType(request.isUpdateType() ? 1 : 0));
playlistSongs.add(new SpecifiedTotalCount(itemCount.getValue()));
playlistSongs.add(new ReturnedCount(getSongCount()));
Listing listing = new Listing();
for (Song song : songs) {
ListingItem listingItem = new ListingItem();
for (String key : request.getMeta()) {
Chunk chunk = song.getChunk(key);
if (chunk != null) {
listingItem.add(chunk);
} else if (LOG.isInfoEnabled()) {
LOG.info("Unknown chunk type: " + key);
}
}
listing.add(listingItem);
}
playlistSongs.add(listing);
if (request.isUpdateType() && deletedSongs != null) {
DeletedIdListing deletedListing = new DeletedIdListing();
for (Song song : deletedSongs) {
deletedListing.add(song.getChunk("dmap.itemid"));
}
playlistSongs.add(deletedListing);
}
return playlistSongs;
}
protected boolean getBooleanValue(BooleanChunk chunk) {
return (chunk != null) ? chunk.getBooleanValue() : false;
}
protected int getUByteValue(UByteChunk chunk) {
return (chunk != null) ? chunk.getValue() : 0;
}
protected String getStringValue(StringChunk chunk) {
return (chunk != null) ? chunk.getValue() : null;
}
protected Txn createNewTxn(final String name, boolean value) {
return createNewTxn(name, boolean.class, new Boolean(value));
}
protected Txn createNewTxn(final String name, int value) {
return createNewTxn(name, int.class, new Integer(value));
}
protected Txn createNewTxn(final String name, long value) {
return createNewTxn(name, long.class, new Long(value));
}
protected Txn createNewTxn(final String name, String value) {
return createNewTxn(name, String.class, value);
}
protected Txn createNewTxn(final String fieldName,
final Class<?> valueClass, final Object value) {
Txn txn = new Txn() {
public void commit(Transaction txn) {
setValue(fieldName, valueClass, value);
}
};
return txn;
}
protected void setValue(String fieldName, boolean value) {
setValue(fieldName, boolean.class, new Boolean(value));
}
protected void setValue(String fieldName, int value) {
setValue(fieldName, int.class, new Integer(value));
}
protected void setValue(String fieldName, long value) {
setValue(fieldName, long.class, new Long(value));
}
protected void setValue(String fieldName, String value) {
setValue(fieldName, String.class, value);
}
protected void setValue(String fieldName, Class<?> valueClass, Object value) {
try {
Field field = Playlist.class.getDeclaredField(fieldName);
field.setAccessible(true);
Chunk chunk = (Chunk) field.get(this);
if (chunk == null) {
Constructor<?> con = field.getType().getConstructor(
new Class[] { valueClass });
chunk = (Chunk) con.newInstance(new Object[] { value });
field.set(this, chunk);
addChunk(chunk);
} else {
Method method = field.getType().getMethod("setValue",
new Class[] { valueClass });
method.invoke(chunk, new Object[] { value });
}
} catch (SecurityException e) {
throw new RuntimeException(e);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
}
}
}