/***
** @(#) TradeCard.com 1.0
**
** Copyright (c) 2010 TradeCard, Inc. All Rights Reserved.
**
**
** THIS COMPUTER SOFTWARE IS THE PROPERTY OF TradeCard, Inc.
**
** Permission is granted to use this software as specified by the TradeCard
** COMMERCIAL LICENSE AGREEMENT. You may use this software only for
** commercial purposes, as specified in the details of the license.
** TRADECARD SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY
** THE LICENSEE AS A RESULT OF USING OR MODIFYING THIS SOFTWARE IN ANY WAY.
**
** YOU MAY NOT DISTRIBUTE ANY SOURCE CODE OR OBJECT CODE FROM THE TradeCard.com
** TOOLKIT AT ANY TIME. VIOLATORS WILL BE PROSECUTED TO THE FULLEST EXTENT
** OF UNITED STATES LAW.
**
** @version 1.0
** @author Copyright (c) 2010 TradeCard, Inc. All Rights Reserved.
**
**/
package com.partydj.server;
import java.io.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.regex.*;
import javax.sound.sampled.*;
import com.google.common.collect.*;
import com.partydj.player.*;
import com.partydj.util.*;
/**
*
**/
public enum PlaylistManager {
INSTANCE;
public static final int MIN_QUEUE_SIZE = 3;
private static final ScheduledExecutorService CHECKER_POOL = Executors.newScheduledThreadPool(1, NamedThreadFactory.createDaemonFactory("Playlist Manager"));
// private static ExecutorService SONG_POOL_BUILDER_POOL = Executors.newFixedThreadPool(5, NamedThreadFactory.createDaemonFactory("Playlist Song Pool Builder"));
private QueueChecker CHECKER = new QueueChecker();
private List<MediaRequest> requestQueue = Collections.synchronizedList(new LinkedList());
private List<MediaFile> songPool = new ArrayList(); //this is made concurrent after initialization
private int poolPointer = 0;
private Map<MediaFile, Integer> requestCount = new ConcurrentHashMap<MediaFile, Integer>();
private Map<MediaFile, Long> lastPlayed = new ConcurrentHashMap<MediaFile, Long>();
private Collection<MediaFile> history = new ConcurrentLinkedQueue();
private SortedSetMultimap<String, Long> skips = TreeMultimap.create();
void start() {
String poolFile = Config.config().getProperty(ConfigKeys.MUSIC_POOL);
File songPoolSource = new File(poolFile);
Player player = PartyDJ.getInstance().getPlayer();
player.ensureAvailable();
if (songPoolSource != null && songPoolSource.exists()) {
if (songPoolSource.isDirectory()) {
createPoolFromDirectory(songPoolSource);
} else {
createPoolFromPlaylist(songPoolSource);
}
if (Config.config().getBooleanProperty(ConfigKeys.RANDOMIZE_POOL)) {
Collections.shuffle(songPool);
}
songPool = new CopyOnWriteArrayList(songPool);
}
int next = queueNext(MIN_QUEUE_SIZE - player.getPlayQueueSize());
CHECKER_POOL.schedule(CHECKER, next, TimeUnit.SECONDS);
}
static final FileFilter POOL_FILE_FILTER = new FileFilter() {
private final Pattern songFilePattern = Pattern.compile("\\.mp3$|\\.m4a$", Pattern.CASE_INSENSITIVE);
private static final long MIN_SIZE = 349525; //third of a meg
@Override public boolean accept(File pathname) {
return (!isSymlink(pathname) && pathname.isDirectory()) || (pathname.length() >= MIN_SIZE && songFilePattern.matcher(pathname.getName()).find());
}
boolean isSymlink(File file) {
try {
File canon;
if (file.getParent() == null) {
canon = file;
} else {
File canonDir = file.getParentFile().getCanonicalFile();
canon = new File(canonDir, file.getName());
}
return !canon.getCanonicalFile().equals(canon.getAbsoluteFile());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
};
private void createPoolFromDirectory(final File songPoolSource) {
// SONG_POOL_BUILDER_POOL.execute(new Runnable() {
// @Override public void run() {
System.out.println("Adding From: " + songPoolSource.getAbsolutePath());
File[] files = songPoolSource.listFiles(POOL_FILE_FILTER);
for (File file : files) {
if (file.isDirectory()) {
createPoolFromDirectory(file);
} else {
addToPool(MediaFile.create(file));
}
}
// }
// });
}
private void createPoolFromPlaylist(File songPoolSource) {
try {
ChunkedCharBuffer fileContents = new ChunkedCharBuffer();
final FileReader fr = new FileReader(songPoolSource);
try {
fileContents.append(fr);
} finally {
fr.close();
}
if (fileContents.length() > 0) {
for (String fileName : fileContents.split("\\s*\\r?\\n\\s*")) {
if (!fileName.startsWith("#")) { //ignore comments - this will make playlist files usable (if they include the fill path to the file)
MediaFile file = MediaFile.create(fileName);
if (file != null) {
addToPool(file);
}
}
}
}
} catch (Exception e) {
System.out.println("Error reading playlist file: " + e.getMessage());
e.printStackTrace();
}
}
public Collection<MediaRequest> getRequests() {
return Collections.unmodifiableCollection(requestQueue);
}
public int request(MediaFile mediaFile, String from) {
if (mediaFile != null && allowQueue(mediaFile)) {
Player player = PartyDJ.getInstance().getPlayer();
int totalSeconds = player.getTotalQueueLengthInSeconds();
MediaRequest existingRequest = null;
for (MediaRequest requestItem : requestQueue) {
if (requestItem.getMediaFile().equals(mediaFile)) {
existingRequest = requestItem;
break;
}
try {
totalSeconds += requestItem.getMediaFile().getMetadata().getDurationSeconds().intValue();
} catch (Exception e) {
AudioFileFormat baseFileFormat;
try {
baseFileFormat = AudioSystem.getAudioFileFormat(requestItem.getMediaFile().getFile());
Map properties = baseFileFormat.properties();
totalSeconds += (Long) properties.get("duration");
} catch (Exception e1) {
totalSeconds += 5;
}
}
}
if (existingRequest != null) {
existingRequest.vote(from);
} else {
increaseRequestCount(mediaFile);
requestQueue.add(MediaRequest.create(mediaFile, from));
}
Collections.sort(requestQueue);
if (!player.isPlaying()) {
CHECKER_POOL.schedule(CHECKER, 10, TimeUnit.MILLISECONDS);
}
return totalSeconds;
}
return 0;
}
public boolean canSkip(String who) {
Integer maxSkipsPerHour = Config.config().getIntegerProperty(ConfigKeys.MAX_SKIPS_PER_HOUR);
if (maxSkipsPerHour != null) {
long now = System.currentTimeMillis();
long ago = now - 1000 * 60 * 60; //1 hour
SortedSet<Long> userSkips = skips.get(who);
if (userSkips.isEmpty()) {
return true;
} else {
//first remove/cleanup stale entries
Iterator<Long> it = userSkips.iterator();
while (it.hasNext() && it.next().longValue() < ago) {
it.remove();
}
if (userSkips.size() < maxSkipsPerHour) {
return true;
}
}
}
return false;
}
public void skipToNext(String who) {
if (canSkip(who)) {
Player player = PartyDJ.getInstance().getPlayer();
player.skipToNextInQueue();
skips.put(who, Long.valueOf(System.currentTimeMillis()));
}
}
public void addToPool(MediaFile file) {
if (songPool == null) {
songPool = new ArrayList();
}
songPool.add(file);
PartyDJ.getInstance().getSearchProvider().addToSearchIndex(file);
}
protected MediaFile getNextMediaFile() {
if (!requestQueue.isEmpty()) {
Iterator<MediaRequest> requestIt = requestQueue.iterator();
MediaRequest request = requestIt.next();
requestIt.remove();
return request.getMediaFile();
} else if (!songPool.isEmpty()) {
MediaFile next = songPool.get(poolPointer++);
if (poolPointer >= songPool.size()) {
poolPointer = 0;
Collections.shuffle(songPool);
}
return next;
}
return null;
}
private int increaseRequestCount(MediaFile file) {
Integer currentRequestCount = requestCount.get(file);
if (currentRequestCount == null) {
currentRequestCount = Integer.valueOf(0);
}
requestCount.put(file, Integer.valueOf(currentRequestCount.intValue() + 1));
return currentRequestCount.intValue() + 1;
}
private int getRequestCount(MediaFile file) {
Integer currentRequestCount = requestCount.get(file);
if (currentRequestCount == null) {
currentRequestCount = Integer.valueOf(0);
}
return currentRequestCount.intValue();
}
protected final int queueNext(int size) {
boolean added = false;
int next = 5;
Player player = PartyDJ.getInstance().getPlayer();
if (size > 0) {
for (int i = 0; i < size; i++) {
MediaFile file = getNextMediaFile();
if (file != null) {
history.add(file);
next = player.addToQueue(file);
added = true;
}
}
}
if (!added) {
next = 15;
}
if (!player.isPaused()) {
player.ensurePlaying();
}
return next;
}
private boolean allowQueue(MediaFile file) {
int playCount = getRequestCount(file);
long lastPlayed = getLastPlayed(file);
return file != null &&
(getMaxAllowedPlayCount() == null || playCount < getMaxAllowedPlayCount().intValue()) &&
(lastPlayed == -1 || lastPlayed > getReplayTimeThreshhold())
;
}
/**
* @return the last time (in minutes) that the given song was played OR -1 if it has not yet been played
*/
public long getLastPlayed(MediaFile file) {
Long lastTS = lastPlayed.get(file);
return lastTS != null ? ((System.currentTimeMillis() - lastTS.intValue()) / 60000) : -1;
}
private Integer getMaxAllowedPlayCount() {
return Config.config().getIntegerProperty(ConfigKeys.MAX_ALLOWED_PLAY_COUNT);
}
private int getReplayTimeThreshhold() {
return Config.config().getIntProperty(ConfigKeys.REPLAY_TIME_THRESHHOLD);
}
public Collection<MediaFile> getHistory() {
return Collections.unmodifiableCollection(history);
}
class QueueChecker implements Runnable {
@Override public void run() {
int next = 15;
try {
Player player = PartyDJ.getInstance().getPlayer();
next = queueNext(MIN_QUEUE_SIZE - player.getPlayQueueSize());
} catch (Throwable t) {
try {
Player player = PartyDJ.getInstance().getPlayer();
if (!player.isAvailable()) {
player.ensureAvailable();
System.out.println("Player was not available, but not is ready. Requeueing in 2 seconds.");
next = 2;
} else {
System.out.println("MAJOR error in queue checker. Rescheduling for 1 minute from now and crossing fingers.");
next = 60;
}
} catch (Exception e) {
System.out.println("Player cannot be made available! Maybe someone will do somethign to change that in the next 5 minutes.");
next = 300;
}
} finally {
CHECKER_POOL.schedule(CHECKER, next, TimeUnit.SECONDS);
}
}
}
}