/*
* 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.util;
import ext.ProcessLauncher;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.CharUtils;
import org.apache.commons.lang.StringUtils;
import org.jajuk.base.FileManager;
import org.jajuk.base.Playlist;
import org.jajuk.base.PlaylistManager;
import org.jajuk.base.SmartPlaylist;
import org.jajuk.events.JajukEvent;
import org.jajuk.events.JajukEvents;
import org.jajuk.events.ObservationManager;
import org.jajuk.services.dj.Ambience;
import org.jajuk.services.dj.AmbienceManager;
import org.jajuk.services.dj.DigitalDJ;
import org.jajuk.services.dj.DigitalDJManager;
import org.jajuk.ui.helpers.StarsHelper;
import org.jajuk.util.error.JajukException;
import org.jajuk.util.log.Log;
/**
* Utilities for the Prepare Party Wizard. Extracted into a separate class for
* easier testing.
*/
public class UtilPrepareParty {
/** character that is used to replace if filename normalization is used. */
private static final String FILLER_CHAR = "_";
/**
* Instantiates a new util prepare party.
*
* private constructor to avoid instantiation
*/
private UtilPrepareParty() {
}
/**
* Filter provided list by removing files that have lower rating.
*
* @param files the list to process.
* @param rate The require rating level
*
* @return The adjusted list.
*/
public static List<org.jajuk.base.File> filterRating(List<org.jajuk.base.File> files, Integer rate) {
final List<org.jajuk.base.File> newFiles = new ArrayList<org.jajuk.base.File>();
for (org.jajuk.base.File file : files) {
// only add files that have a rate equal or higher than the level set
if (StarsHelper.getStarsNumber(file.getTrack()) >= rate) {
newFiles.add(file);
}
}
return newFiles;
}
/**
* Filter the provided list by removing files if the specified length (in
* minutes) is exceeded.
*
* @param files The list of files to process.
* @param time The number of minutes playing length to have at max.
*
* @return The modified list.
*/
public static List<org.jajuk.base.File> filterMaxLength(List<org.jajuk.base.File> files,
Integer time) {
final List<org.jajuk.base.File> newFiles = new ArrayList<org.jajuk.base.File>();
long accumulated = 0;
for (org.jajuk.base.File file : files) {
// check if we now exceed the max length, getDuration() is in seconds, but
// we want to use minutes
if ((accumulated + file.getTrack().getDuration()) / 60 > time) {
return newFiles;
}
accumulated += file.getTrack().getDuration();
newFiles.add(file);
}
// there were not enough files to reach the limit, return the full list
return files;
}
/**
* Filter the provided list by removing files after the specified size is
* reached.
*
* @param files The list of files to process.
* @param size The size in MB that should not be exceeded.
*
* @return The modified list.
*/
public static List<org.jajuk.base.File> filterMaxSize(List<org.jajuk.base.File> files,
Integer size) {
final List<org.jajuk.base.File> newFiles = new ArrayList<org.jajuk.base.File>();
long accumulated = 0;
for (org.jajuk.base.File file : files) {
// check if we now exceed the max size, getSize() is in byte, but we want
// to use MB
if ((accumulated + file.getSize()) / (1024 * 1024) > size) {
return newFiles;
}
accumulated += file.getSize();
newFiles.add(file);
}
// there were not enough files to reach the limit, return the full list
return files;
}
/**
* Filter the provided list by removing files after the specified number of
* tracks is reached.
*
* @param files The list of files to process.
* @param tracks The number of tracks to limit the list.
*
* @return The modified list.
*/
public static List<org.jajuk.base.File> filterMaxTracks(List<org.jajuk.base.File> files,
Integer tracks) {
final List<org.jajuk.base.File> newFiles = new ArrayList<org.jajuk.base.File>();
int count = 0;
for (org.jajuk.base.File file : files) {
// check if we have reached the max
if (count > tracks) {
return newFiles;
}
count++;
newFiles.add(file);
}
// there were not enough files to reach the limit, return the full list
return files;
}
/**
* Filter the provided list by removing files so only the specified media is
* included.
*
* @param files The list of files to process.
* @param ext The number of tracks to filter the list.
*
* @return The modified list.
*/
public static List<org.jajuk.base.File> filterMedia(final List<org.jajuk.base.File> files,
final String ext) {
final List<org.jajuk.base.File> newFiles = new ArrayList<org.jajuk.base.File>();
for (org.jajuk.base.File file : files) {
if (file.getType() != null && file.getType().getExtension() != null
&& file.getType().getExtension().equals(ext)) {
newFiles.add(file);
}
}
return newFiles;
}
/** Map containing all the replacements that we do to "normalize" a filename. */
private static Map<Character, String> replaceMap = null;
/**
* Normalize filenames so that they do not.
*
* TODO: is there some utility method that can do this?
*
* @param name Name that should be normalized
*
* @return the filename where special characters are replaced/removed
*/
public static synchronized String normalizeFilename(String name) {
// initialize map if necessary
if (replaceMap == null) {
replaceMap = new HashMap<Character, String>();
// German umlauts can be handled better than just using the filler_char,
// we
// can keep the filename readable
replaceMap.put('à', "a");
replaceMap.put('á', "a");
replaceMap.put('â', "a");
replaceMap.put('ã', "a");
replaceMap.put('ä', "ae");
replaceMap.put('å', "a");
replaceMap.put('æ', "ae");
replaceMap.put('À', "A");
replaceMap.put('Á', "A");
replaceMap.put('Â', "A");
replaceMap.put('Ã', "A");
replaceMap.put('Ä', "AE");
replaceMap.put('Å', "A");
replaceMap.put('Æ', "AE");
replaceMap.put('Ç', "C");
replaceMap.put('ç', "c");
replaceMap.put('Ð', "D");
replaceMap.put('È', "E");
replaceMap.put('É', "E");
replaceMap.put('Ê', "E");
replaceMap.put('Ë', "E");
replaceMap.put('é', "e");
replaceMap.put('è', "e");
replaceMap.put('é', "e");
replaceMap.put('ê', "e");
replaceMap.put('ë', "e");
replaceMap.put('Ì', "I");
replaceMap.put('Í', "I");
replaceMap.put('Î', "I");
replaceMap.put('Ï', "I");
replaceMap.put('ì', "i");
replaceMap.put('í', "i");
replaceMap.put('î', "i");
replaceMap.put('ï', "i");
replaceMap.put('Ñ', "N");
replaceMap.put('ñ', "n");
replaceMap.put('Ò', "O");
replaceMap.put('Ó', "O");
replaceMap.put('Ô', "O");
replaceMap.put('Õ', "O");
replaceMap.put('Ö', "OE");
replaceMap.put('Ő', "O");
replaceMap.put('Œ', "O");
replaceMap.put('ò', "o");
replaceMap.put('ó', "o");
replaceMap.put('ô', "o");
replaceMap.put('õ', "o");
replaceMap.put('ö', "oe");
replaceMap.put('ő', "o");
replaceMap.put('œ', "oe");
replaceMap.put('ß', "ss");
replaceMap.put('Ù', "U");
replaceMap.put('Ú', "U");
replaceMap.put('Û', "U");
replaceMap.put('Ü', "UE");
replaceMap.put('ù', "u");
replaceMap.put('ú', "u");
replaceMap.put('û', "u");
replaceMap.put('ü', "ue");
replaceMap.put('Ý', "Y");
replaceMap.put('ý', "y");
replaceMap.put('ÿ', "y");
// some more special characters that can be replaced with more useful
// values
// than FILLER_CHAR
replaceMap.put('€', "EUR");
replaceMap.put('&', "and");
// replace path-separators and colon that could cause trouble on other
// OSes, also question mark and star can produce errors
replaceMap.put('/', FILLER_CHAR);
replaceMap.put('\\', FILLER_CHAR);
replaceMap.put(':', FILLER_CHAR);
replaceMap.put('?', FILLER_CHAR);
replaceMap.put('*', FILLER_CHAR);
replaceMap.put('!', FILLER_CHAR);
}
StringBuilder newName = new StringBuilder(name.length());
for (int i = 0; i < name.length(); i++) {
char c = name.charAt(i);
// replace some things that we can replace with other useful values
if (replaceMap.containsKey(c)) {
newName.append(replaceMap.get(c));
} else if (CharUtils.isAsciiPrintable(c)) {
// any other ASCII character is added
newName.append(c);
} else {
// everything else outside the ASCII range is simple removed to not
// cause any trouble
newName.append(FILLER_CHAR);
}
}
return newName.toString();
}
/**
* Get files from the specified DJ.
*
* @param name The name of the DJ.
*
* @return A list of files.
*/
public static List<org.jajuk.base.File> getDJFiles(final String name) {
DigitalDJ dj = DigitalDJManager.getInstance().getDJByName(name);
return dj.generatePlaylist();
}
/**
* Get files from the specified Ambience.
*
* @param name The name of the Ambience.
*
* @return A list of files.
*/
public static List<org.jajuk.base.File> getAmbienceFiles(String name) {
final List<org.jajuk.base.File> files;
Ambience ambience = AmbienceManager.getInstance().getAmbienceByName(name);
files = new ArrayList<org.jajuk.base.File>();
// Get a shuffle selection
List<org.jajuk.base.File> allFiles = FileManager.getInstance().getGlobalShufflePlaylist();
// Keep only right genres and check for unicity
for (org.jajuk.base.File file : allFiles) {
if (ambience.getGenres().contains(file.getTrack().getGenre())) {
files.add(file);
}
}
return files;
}
/**
* Get files from the specified Playlist. If the name of the playlist is equal
* to the name of the temporary playlist provided to the Wizard, then this
* Playlist is used instead.
*
* @param name The name of the Playlist.
* @param tempPlaylist The playlist provided upon starting of the Wizard, null if none
* provided.
*
* @return A list of files.
*
* @throws JajukException the jajuk exception
*/
public static List<org.jajuk.base.File> getPlaylistFiles(String name, Playlist tempPlaylist)
throws JajukException {
// if we chose the temp-playlist, use this one
if (tempPlaylist != null && name.equals(tempPlaylist.getName())) {
return tempPlaylist.getFiles();
}
// get the Playlist from the Manager by name
Playlist playlist = PlaylistManager.getInstance().getPlaylistByName(name);
return playlist.getFiles();
}
/**
* Get files in random order.
*
* @return Returns a list of all files shuffled into random order.
*/
public static List<org.jajuk.base.File> getShuffleFiles() {
// Get a shuffle selection from all files
return FileManager.getInstance().getGlobalShufflePlaylist();
}
/**
* Get files from the BestOf-Playlist.
*
* @return The list of files that match the "BestOf"-criteria
*
* @throws JajukException the jajuk exception
*/
public static List<org.jajuk.base.File> getBestOfFiles() throws JajukException {
Playlist pl = new SmartPlaylist(Playlist.Type.BESTOF, "tmp", "temporary", null);
return pl.getFiles();
}
/**
* Get the files from the current "Novelties"-criteria.
*
* @return The files that are new currently.
*
* @throws JajukException the jajuk exception
*/
public static List<org.jajuk.base.File> getNoveltiesFiles() throws JajukException {
Playlist pl = new SmartPlaylist(Playlist.Type.NOVELTIES, "tmp", "temporary", null);
return pl.getFiles();
}
/**
* Get the files from the current Queue.
*
* @return The currently queued files.
*
* @throws JajukException the jajuk exception
*/
public static List<org.jajuk.base.File> getQueueFiles() throws JajukException {
Playlist pl = new SmartPlaylist(Playlist.Type.QUEUE, "tmp", "temporary", null);
return pl.getFiles();
}
/**
* Get the files that are bookmarked.
*
* @return The currently bookmarked files.
*
* @throws JajukException the jajuk exception
*/
public static List<org.jajuk.base.File> getBookmarkFiles() throws JajukException {
Playlist pl = new SmartPlaylist(Playlist.Type.BOOKMARK, "tmp", "temporary", null);
return pl.getFiles();
}
/**
* Split the commandline into separate elements by observing double quotes.
*
* @param command The command in one string. E.g. "perl /usr/bin/pacpl".
*
* @return A list of single command elements. e.g. {"perl", "/usr/bin/pacpl"}
*/
private static List<String> splitCommand(String command) {
List<String> list = new ArrayList<String>();
StringBuilder word = new StringBuilder();
boolean quote = false;
int i = 0;
while (i < command.length()) {
char c = command.charAt(i);
// word boundary
if (Character.isWhitespace(c) && !quote) {
i++;
// finish current word
list.add(word.toString());
word = new StringBuilder();
// skip more whitespaces
while (Character.isWhitespace(command.charAt(i)) && i < command.length()) {
i++;
}
} else {
// on quote we either start or end a quoted string
if (c == '"') {
quote = !quote;
}
word.append(c);
i++;
}
}
// finish last word
if (word.length() > 0) {
list.add(word.toString());
}
return list;
}
/**
* Check if the Perl Audio Converter can be used.
*
* @param pacpl The command-string to call pacpl, e.g. "pacpl" or "perl
* C:\pacpl\pacpl", ...
*
* @return true, if check pacpl
*/
public static boolean checkPACPL(String pacpl) {
// here we just want to verify that we find pacpl
// first build the commandline for "pacpl --help"
// see the manual page of "pacpl"
List<String> list = splitCommand(pacpl);
list.add("--help");
// create streams for catching stdout and stderr
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayOutputStream err = new ByteArrayOutputStream();
int ret = 0;
final ProcessLauncher launcher = new ProcessLauncher(out, err, 10000);
try {
ret = launcher.exec(list.toArray(new String[list.size()]));
} catch (IOException e) {
ret = -1;
Log.debug("Exception while checking for 'pacpl', cannot use functionality to convert media files while copying: "
+ e.getMessage());
}
// if we do not find the application or if we got an error, log some details
// and disable notification support
if (ret != 0) {
// log out the results
Log.debug("pacpl command returned to out(" + ret + "): " + out.toString());
Log.debug("pacpl command returned to err: " + err.toString());
Log.info("Cannot use functionality to convert media files, application 'pacpl' seems to be not available correctly.");
return false;
}
// pacpl is enabled and seems to be supported by the OS
return true;
}
/**
* Call the external application "pacpl" to convert the specified file into
* the specified format and store the resulting file in the directory listed.
*
* @param pacpl The command-string to call pacpl, e.g. "pacpl" or "perl
* C:\pacpl\pacpl", ...
* @param file The file to convert.
* @param toFormat The target format.
* @param toDir The target location.
* @param newName The new name to use (this is used for normalizing and numbering
* the files, ...)
*
* @return 0 if processing was OK, otherwise the return code indicates the
* return code provided by the pacpl script
*
* TODO: currently this uses the target-location as temporary
* directory if intermediate-conversion to WAV is necessary, this
* might be sub-optimal for Flash-memory where too many writes kills
* the media card earlier. We probably should use the temporary
* directory for conversion instead and do another copy at the end.
*/
public static int convertPACPL(String pacpl, File file, String toFormat, java.io.File toDir,
String newName) {
// first build the commandline for "pacpl"
// see the manual page of "pacpl"
// first split the command itself with observing quotes, splitting is
// necessary because it can be something like "perl <locatoin>/pacpl"
List<String> list = splitCommand(pacpl);
// where to store the file
list.add("--outdir");
list.add(toDir.getAbsolutePath());
// specify new filename
list.add("--outfile");
list.add(newName);
// specify output format
list.add("--to");
list.add(toFormat);
// now add the actual file to convert
list.add(file.getAbsolutePath());
// create streams for catching stdout and stderr
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayOutputStream err = new ByteArrayOutputStream();
int ret = 0;
StringBuilder commandLog = new StringBuilder();
for (String arg : list) {
commandLog.append(arg + " ");
}
Log.debug("Using this pacpl command: {{" + commandLog.toString() + "}}");
final ProcessLauncher launcher = new ProcessLauncher(out, err);
try {
ret = launcher.exec(list.toArray(new String[list.size()]), null,
new java.io.File(System.getProperty("java.io.tmpdir")));
} catch (IOException e) {
ret = -1;
Log.error(e);
}
// log out the results
if (!out.toString().isEmpty()) {
Log.debug("pacpl command returned to out(" + ret + "): " + out.toString());
if (out.toString().indexOf("encode failed") != -1) {
ret = -1;
}
} else {
Log.debug("pacpl command returned: " + ret);
}
if (!err.toString().isEmpty()) {
Log.debug("pacpl command returned to err: " + err.toString());
if (err.toString().indexOf("encode failed") != -1) {
ret = -1;
}
}
return ret;
}
/**
* Copies the files contained in the list to the specified directory.
*
* @param files The list of flies to copy.
* @param destDir The target location.
* @param isNormalize
* @param isConvertMedia
* @param media
* @param convertCommand
*/
public static void copyFiles(final List<org.jajuk.base.File> files, final java.io.File destDir,
final boolean isNormalize, final boolean isConvertMedia, final String media,
final String convertCommand) {
Thread thread = new Thread("PrepareParty - File Copy") {
@Override
public void run() {
UtilGUI.waiting();
// start time to display elapsed time at the end
long lRefreshDateStart = System.currentTimeMillis();
// start copying and create a playlist on the fly
int convert_errors = 0;
final java.io.File file = new java.io.File(destDir.getAbsolutePath() + "/playlist.m3u");
try {
final BufferedWriter bw = new BufferedWriter(new FileWriter(file));
try {
bw.write(Const.PLAYLIST_NOTE);
int count = 0;
for (final org.jajuk.base.File entry : files) {
// update progress
count++;
// We can use the actual file name as we do numbering of the files,
// this is important for existing playlists to keep the order
String name = StringUtils.leftPad(Integer.valueOf(count).toString(), 5, '0') + '_'
+ entry.getFIO().getName();
// normalize filenames if necessary
if (isNormalize) {
name = UtilPrepareParty.normalizeFilename(name);
}
// check if we need to convert the file format
if (isConvertMedia && !entry.getType().getExtension().equals(media)) {
// Notify that we are converting a file
Properties properties = new Properties();
properties.put(Const.DETAIL_CONTENT, entry.getName());
properties.put(Const.DETAIL_NEW, name + "." + media);
ObservationManager.notify(new JajukEvent(JajukEvents.FILE_CONVERSION, properties));
int ret = UtilPrepareParty.convertPACPL(convertCommand, entry.getFIO(), media,
destDir, name);
if (ret != 0) {
convert_errors++;
// do a normal copy of original format if it cannot be converted
FileUtils.copyFile(entry.getFIO(), new File(destDir, name));
} else {
// Conversion is done, new filename is <oldname.old_extension.target_extension>
name = name + "." + media;
}
} else {
// do a normal copy otherwise
FileUtils.copyFile(entry.getFIO(), new File(destDir, name));
}
// increase hits for this track/file as it is likely played outside of Jajuk
entry.getTrack().incHits();
// write playlist as well
bw.newLine();
bw.write(name);
// Notify that a file has been copied
Properties properties = new Properties();
properties.put(Const.DETAIL_CONTENT, entry.getName());
ObservationManager.notify(new JajukEvent(JajukEvents.FILE_COPIED, properties));
}
bw.flush();
} finally {
bw.close();
}
// Send a last event with null properties to inform the
// client that the party is done
ObservationManager.notify(new JajukEvent(JajukEvents.FILE_COPIED));
} catch (final IOException e) {
Log.error(e);
Messages.showErrorMessage(180, e.getMessage());
return;
} finally {
long refreshTime = System.currentTimeMillis() - lRefreshDateStart;
// inform the user about the number of resulting tracks
StringBuilder sbOut = new StringBuilder();
sbOut.append(Messages.getString("PreparePartyWizard.31")).append(" ")
.append(destDir.getAbsolutePath()).append(".\n").append(files.size()).append(" ")
.append(Messages.getString("PreparePartyWizard.23")).append(" ")
.append(((refreshTime < 1000) ? refreshTime + " ms." : refreshTime / 1000 + " s."));
// inform user if converting did not work
if (convert_errors > 0) {
sbOut.append("\n").append(Integer.toString(convert_errors))
.append(Messages.getString("PreparePartyWizard.36"));
}
String message = sbOut.toString();
Log.debug(message);
UtilGUI.stopWaiting();
// Display end of copy message with stats
Messages.showInfoMessage(message);
}
}
};
thread.start();
}
}