diff --git a/build.xml b/build.xml
index d607d2f5..236bf092 100644
--- a/build.xml
+++ b/build.xml
@@ -109,8 +109,8 @@
-
-
+
+
diff --git a/installer/webstart/filebot.jnlp b/installer/webstart/filebot.jnlp
index 3a04e358..a7226e00 100644
--- a/installer/webstart/filebot.jnlp
+++ b/installer/webstart/filebot.jnlp
@@ -49,7 +49,7 @@
-
+
diff --git a/lib/commons-io.jar b/lib/commons-io.jar
new file mode 100644
index 00000000..b5c7d692
Binary files /dev/null and b/lib/commons-io.jar differ
diff --git a/lib/guava.jar b/lib/guava.jar
deleted file mode 100644
index d107c0f3..00000000
Binary files a/lib/guava.jar and /dev/null differ
diff --git a/source/net/sourceforge/filebot/cli/CmdlineOperations.java b/source/net/sourceforge/filebot/cli/CmdlineOperations.java
index 63b02339..ce39aae9 100644
--- a/source/net/sourceforge/filebot/cli/CmdlineOperations.java
+++ b/source/net/sourceforge/filebot/cli/CmdlineOperations.java
@@ -3,7 +3,6 @@ package net.sourceforge.filebot.cli;
import static java.lang.String.*;
-import static java.util.Arrays.*;
import static java.util.Collections.*;
import static net.sourceforge.filebot.MediaTypes.*;
import static net.sourceforge.filebot.WebServices.*;
@@ -47,6 +46,7 @@ import net.sourceforge.filebot.format.MediaBindingBean;
import net.sourceforge.filebot.hash.HashType;
import net.sourceforge.filebot.hash.VerificationFileReader;
import net.sourceforge.filebot.hash.VerificationFileWriter;
+import net.sourceforge.filebot.media.ReleaseInfo;
import net.sourceforge.filebot.similarity.EpisodeMetrics;
import net.sourceforge.filebot.similarity.Match;
import net.sourceforge.filebot.similarity.Matcher;
@@ -260,49 +260,68 @@ public class CmdlineOperations implements CmdlineInterface {
}
- public List renameMovie(Collection mediaFiles, String query, ExpressionFormat format, MovieIdentificationService service, Locale locale, boolean strict) throws Exception {
+ public List renameMovie(Collection files, String query, ExpressionFormat format, MovieIdentificationService service, Locale locale, boolean strict) throws Exception {
CLILogger.config(format("Rename movies using [%s]", service.getName()));
// handle movie files
- File[] movieFiles = filter(mediaFiles, VIDEO_FILES).toArray(new File[0]);
- File[] subtitleFiles = filter(mediaFiles, SUBTITLE_FILES).toArray(new File[0]);
- Movie[] movieByFileHash = new Movie[movieFiles.length];
+ List movieFiles = filter(files, VIDEO_FILES);
- if (movieFiles.length > 0 && query == null) {
- // match movie hashes online
+ Map> derivatesByMovieFile = new HashMap>();
+ for (File movieFile : movieFiles) {
+ derivatesByMovieFile.put(movieFile, new ArrayList());
+ }
+ for (File file : files) {
+ for (File movieFile : movieFiles) {
+ if (!file.equals(movieFile) && isDerived(file, movieFile)) {
+ derivatesByMovieFile.get(movieFile).add(file);
+ break;
+ }
+ }
+ }
+
+ List standaloneFiles = new ArrayList(files);
+ for (List derivates : derivatesByMovieFile.values()) {
+ standaloneFiles.removeAll(derivates);
+ }
+
+ List movieMatchFiles = new ArrayList();
+ movieMatchFiles.addAll(movieFiles);
+ movieMatchFiles.addAll(filter(files, new ReleaseInfo().getDiskFolderFilter()));
+ movieMatchFiles.addAll(filter(standaloneFiles, SUBTITLE_FILES));
+
+ // map movies to (possibly multiple) files (in natural order)
+ Map> filesByMovie = new HashMap>();
+
+ // match movie hashes online
+ Map movieByFile = new HashMap();
+ if (query == null && movieFiles.size() > 0) {
try {
CLILogger.fine(format("Looking up movie by filehash via [%s]", service.getName()));
- movieByFileHash = service.getMovieDescriptors(movieFiles, locale);
- Analytics.trackEvent(service.getName(), "HashLookup", "Movie", movieByFileHash.length - frequency(asList(movieByFileHash), null)); // number of positive hash lookups
+ Map hashLookup = service.getMovieDescriptors(movieFiles, locale);
+ movieByFile.putAll(hashLookup);
+ Analytics.trackEvent(service.getName(), "HashLookup", "Movie", hashLookup.size()); // number of positive hash lookups
} catch (UnsupportedOperationException e) {
CLILogger.fine(format("%s: Hash lookup not supported", service.getName()));
}
}
- if (subtitleFiles.length > 0 && movieFiles.length == 0) {
- // special handling if there is only subtitle files
- movieByFileHash = new Movie[subtitleFiles.length];
- movieFiles = subtitleFiles;
- subtitleFiles = new File[0];
- }
-
if (query != null) {
CLILogger.fine(format("Looking up movie by query [%s]", query));
Movie result = (Movie) selectSearchResult(query, service.searchMovie(query, locale), strict).get(0);
- fill(movieByFileHash, result);
+ // force all mappings
+ for (File file : movieMatchFiles) {
+ movieByFile.put(file, result);
+ }
}
- // map movies to (possibly multiple) files (in natural order)
- Map> filesByMovie = new HashMap>();
-
// map all files by movie
- for (int i = 0; i < movieFiles.length; i++) {
- Movie movie = movieByFileHash[i];
+ for (File file : movieMatchFiles) {
+ Movie movie = movieByFile.get(file);
// unknown hash, try via imdb id from nfo file
if (movie == null) {
- CLILogger.fine(format("Auto-detect movie from context: [%s]", movieFiles[i]));
- Collection results = detectMovie(movieFiles[i], null, service, locale, strict);
+ CLILogger.fine(format("Auto-detect movie from context: [%s]", file));
+ Collection results = detectMovie(file, null, service, locale, strict);
movie = (Movie) selectSearchResult(query, results, strict).get(0);
if (movie != null) {
@@ -320,7 +339,7 @@ public class CmdlineOperations implements CmdlineInterface {
filesByMovie.put(movie, movieParts);
}
- movieParts.add(movieFiles[i]);
+ movieParts.add(file);
}
}
@@ -341,17 +360,13 @@ public class CmdlineOperations implements CmdlineInterface {
}
matches.add(new Match(file, part));
- }
- }
-
- // handle subtitle files
- for (File subtitle : subtitleFiles) {
- // check if subtitle corresponds to a movie file (same name, different extension)
- for (Match movieMatch : matches) {
- if (isDerived(subtitle, movieMatch.getValue())) {
- matches.add(new Match(subtitle, movieMatch.getCandidate()));
- // movie match found, we're done
- break;
+
+ // automatically add matches for derivates
+ List derivates = derivatesByMovieFile.get(file);
+ if (derivates != null) {
+ for (File derivate : derivates) {
+ matches.add(new Match(derivate, part));
+ }
}
}
}
@@ -387,7 +402,7 @@ public class CmdlineOperations implements CmdlineInterface {
for (Entry it : renameMap.entrySet()) {
try {
// rename file, throw exception on failure
- File destination = renameFile(it.getKey(), it.getValue());
+ File destination = moveRename(it.getKey(), it.getValue());
CLILogger.info(format("Renamed [%s] to [%s]", it.getKey(), it.getValue()));
// remember successfully renamed matches for history entry and possible revert
diff --git a/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy b/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy
index d4cbcb92..29bc91ad 100644
--- a/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy
+++ b/source/net/sourceforge/filebot/cli/ScriptShell.lib.groovy
@@ -37,7 +37,7 @@ File.metaClass.hasExtension = { String... ext -> hasExtension(delegate, ext) }
File.metaClass.isDerived = { f -> isDerived(delegate, f) }
File.metaClass.validateFileName = { validateFileName(delegate) }
File.metaClass.validateFilePath = { validateFilePath(delegate) }
-File.metaClass.moveTo = { f -> renameFile(delegate, f) }
+File.metaClass.moveTo = { f -> moveRename(delegate, f instanceof File ? f : new File(f.toString())) }
List.metaClass.mapByFolder = { mapByFolder(delegate) }
List.metaClass.mapByExtension = { mapByExtension(delegate) }
String.metaClass.getExtension = { getExtension(delegate) }
diff --git a/source/net/sourceforge/filebot/media/MediaDetection.java b/source/net/sourceforge/filebot/media/MediaDetection.java
index 9aa7eabc..95c4e6ff 100644
--- a/source/net/sourceforge/filebot/media/MediaDetection.java
+++ b/source/net/sourceforge/filebot/media/MediaDetection.java
@@ -8,6 +8,7 @@ import static net.sourceforge.filebot.similarity.Normalization.*;
import static net.sourceforge.tuned.FileUtilities.*;
import java.io.File;
+import java.io.FileFilter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
@@ -49,6 +50,11 @@ public class MediaDetection {
private static final ReleaseInfo releaseInfo = new ReleaseInfo();
+ public static boolean isDiskFolder(File folder) {
+ return releaseInfo.getDiskFolderFilter().accept(folder);
+ }
+
+
public static Map, Set> mapSeriesNamesByFiles(Collection files, Locale locale) throws Exception {
SortedMap> filesByFolder = mapByFolder(filter(files, VIDEO_FILES, SUBTITLE_FILES));
@@ -156,8 +162,8 @@ public class MediaDetection {
Set options = new LinkedHashSet();
// lookup by file hash
- if (hashLookupService != null) {
- for (Movie movie : hashLookupService.getMovieDescriptors(new File[] { movieFile }, locale)) {
+ if (hashLookupService != null && movieFile.isFile()) {
+ for (Movie movie : hashLookupService.getMovieDescriptors(singleton(movieFile), locale).values()) {
if (movie != null) {
options.add(movie);
}
diff --git a/source/net/sourceforge/filebot/media/ReleaseInfo.java b/source/net/sourceforge/filebot/media/ReleaseInfo.java
index cff38b07..e625f08c 100644
--- a/source/net/sourceforge/filebot/media/ReleaseInfo.java
+++ b/source/net/sourceforge/filebot/media/ReleaseInfo.java
@@ -2,13 +2,13 @@
package net.sourceforge.filebot.media;
-import static java.util.Arrays.*;
import static java.util.ResourceBundle.*;
import static java.util.regex.Pattern.*;
import static net.sourceforge.filebot.similarity.Normalization.*;
import static net.sourceforge.tuned.StringUtilities.*;
import java.io.File;
+import java.io.FileFilter;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
@@ -125,7 +125,7 @@ public class ReleaseInfo {
languageMap.put(locale.getISO3Language(), locale);
// map display language names for given locales
- for (Locale language : asList(supportedLanguageName)) {
+ for (Locale language : supportedLanguageName) {
languageMap.put(locale.getDisplayLanguage(language), locale);
}
}
@@ -174,6 +174,11 @@ public class ReleaseInfo {
}
+ public FileFilter getDiskFolderFilter() {
+ return new FolderEntryFilter(compile(getBundle(getClass().getName()).getString("pattern.diskfolder.entry")));
+ }
+
+
// fetch release group names online and try to update the data every other day
protected final CachedResource releaseGroupResource = new PatternResource(getBundle(getClass().getName()).getString("url.release-groups"));
protected final CachedResource queryBlacklistResource = new PatternResource(getBundle(getClass().getName()).getString("url.query-blacklist"));
@@ -217,4 +222,28 @@ public class ReleaseInfo {
}
}
+
+ protected static class FolderEntryFilter implements FileFilter {
+
+ private final Pattern entryPattern;
+
+
+ public FolderEntryFilter(Pattern entryPattern) {
+ this.entryPattern = entryPattern;
+ }
+
+
+ @Override
+ public boolean accept(File dir) {
+ if (dir.isDirectory()) {
+ for (String entry : dir.list()) {
+ if (entryPattern.matcher(entry).matches()) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ }
+
}
diff --git a/source/net/sourceforge/filebot/media/ReleaseInfo.properties b/source/net/sourceforge/filebot/media/ReleaseInfo.properties
index cd90096d..256fd507 100644
--- a/source/net/sourceforge/filebot/media/ReleaseInfo.properties
+++ b/source/net/sourceforge/filebot/media/ReleaseInfo.properties
@@ -12,3 +12,6 @@ url.query-blacklist: http://filebot.sourceforge.net/data/query-blacklist.txt
# list of all movies (id, name, year)
url.movie-list: http://filebot.sourceforge.net/data/movies.txt.gz
+
+# disk folder matcher
+pattern.diskfolder.entry: ^BDMV$|^HVDVD_TS$|^VIDEO_TS$|^AUDIO_TS$|^VCD$
diff --git a/source/net/sourceforge/filebot/ui/rename/FilesListTransferablePolicy.java b/source/net/sourceforge/filebot/ui/rename/FilesListTransferablePolicy.java
index c8ff3542..5fa3a89c 100644
--- a/source/net/sourceforge/filebot/ui/rename/FilesListTransferablePolicy.java
+++ b/source/net/sourceforge/filebot/ui/rename/FilesListTransferablePolicy.java
@@ -2,11 +2,14 @@
package net.sourceforge.filebot.ui.rename;
-import static net.sourceforge.tuned.FileUtilities.*;
+import static java.util.Arrays.*;
import java.io.File;
+import java.util.ArrayList;
+import java.util.LinkedList;
import java.util.List;
+import net.sourceforge.filebot.media.MediaDetection;
import net.sourceforge.filebot.ui.transfer.FileTransferablePolicy;
import net.sourceforge.tuned.FastFile;
@@ -15,30 +18,46 @@ class FilesListTransferablePolicy extends FileTransferablePolicy {
private final List model;
-
+
public FilesListTransferablePolicy(List model) {
this.model = model;
}
-
+
@Override
protected boolean accept(List files) {
return true;
}
-
+
@Override
protected void clear() {
model.clear();
}
-
+
@Override
protected void load(List files) {
- model.addAll(FastFile.foreach(flatten(files, 5, false)));
+ List entries = new ArrayList();
+ LinkedList queue = new LinkedList(files);
+
+ while (queue.size() > 0) {
+ File f = queue.removeFirst();
+
+ if (f.isHidden())
+ continue;
+
+ if (f.isFile() || MediaDetection.isDiskFolder(f)) {
+ entries.add(f);
+ } else {
+ queue.addAll(0, asList(f.listFiles()));
+ }
+ }
+
+ model.addAll(FastFile.foreach(entries));
}
-
+
@Override
public String getFileFilterDescription() {
return "files and folders";
diff --git a/source/net/sourceforge/filebot/ui/rename/HistoryDialog.java b/source/net/sourceforge/filebot/ui/rename/HistoryDialog.java
index d90a89f9..680bbec0 100644
--- a/source/net/sourceforge/filebot/ui/rename/HistoryDialog.java
+++ b/source/net/sourceforge/filebot/ui/rename/HistoryDialog.java
@@ -27,8 +27,8 @@ import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
-import java.util.Set;
import java.util.Map.Entry;
+import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
@@ -50,8 +50,8 @@ import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.RowFilter;
-import javax.swing.SortOrder;
import javax.swing.RowSorter.SortKey;
+import javax.swing.SortOrder;
import javax.swing.border.CompoundBorder;
import javax.swing.border.TitledBorder;
import javax.swing.event.DocumentEvent;
@@ -94,7 +94,7 @@ class HistoryDialog extends JDialog {
private final JTable elementTable = createTable(elementModel);
-
+
public HistoryDialog(Window owner) {
super(owner, "Rename History", ModalityType.DOCUMENT_MODAL);
@@ -179,7 +179,7 @@ class HistoryDialog extends JDialog {
private final DateFormat format = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT);
-
+
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
return super.getTableCellRendererComponent(table, format.format(value), isSelected, hasFocus, row, column);
@@ -244,7 +244,7 @@ class HistoryDialog extends JDialog {
setSize(580, 640);
}
-
+
public void setModel(History history) {
// update table model
sequenceModel.setData(history.sequences());
@@ -261,17 +261,17 @@ class HistoryDialog extends JDialog {
initializeInfoLabel();
}
-
+
public History getModel() {
return new History(sequenceModel.getData());
}
-
+
public JLabel getInfoLabel() {
return infoLabel;
}
-
+
private void initializeInfoLabel() {
int count = 0;
Date since = new Date();
@@ -286,7 +286,7 @@ class HistoryDialog extends JDialog {
infoLabel.setText(String.format("A total of %,d files have been renamed since %s.", count, DateFormat.getDateInstance().format(since)));
}
-
+
private JScrollPane createScrollPaneGroup(String title, JComponent component) {
JScrollPane scrollPane = new JScrollPane(component);
scrollPane.setBorder(new CompoundBorder(new TitledBorder(title), scrollPane.getBorder()));
@@ -294,7 +294,7 @@ class HistoryDialog extends JDialog {
return scrollPane;
}
-
+
private JTable createTable(TableModel model) {
JTable table = new JTable(model);
table.setBackground(Color.white);
@@ -312,7 +312,7 @@ class HistoryDialog extends JDialog {
return table;
}
-
+
private final Action closeAction = new AbstractAction("Close") {
@Override
@@ -337,13 +337,13 @@ class HistoryDialog extends JDialog {
maybeShowPopup(e);
}
-
+
@Override
public void mouseReleased(MouseEvent e) {
maybeShowPopup(e);
}
-
+
private void maybeShowPopup(MouseEvent e) {
if (e.isPopupTrigger()) {
JTable table = (JTable) e.getSource();
@@ -383,30 +383,30 @@ class HistoryDialog extends JDialog {
}
};
-
+
private static class RevertAction extends AbstractAction {
public static final String ELEMENTS = "elements";
public static final String PARENT = "parent";
-
+
public RevertAction(Collection elements, HistoryDialog parent) {
putValue(NAME, "Revert...");
putValue(ELEMENTS, elements.toArray(new Element[0]));
putValue(PARENT, parent);
}
-
+
public Element[] elements() {
return (Element[]) getValue(ELEMENTS);
}
-
+
public HistoryDialog parent() {
return (HistoryDialog) getValue(PARENT);
}
-
+
private enum Option {
Rename {
@@ -431,7 +431,7 @@ class HistoryDialog extends JDialog {
}
}
-
+
@Override
public void actionPerformed(ActionEvent e) {
// use default directory
@@ -501,13 +501,13 @@ class HistoryDialog extends JDialog {
}
}
-
+
private void rename(File directory) {
int count = 0;
for (Entry entry : getRenameMap(directory).entrySet()) {
try {
- renameFile(entry.getKey(), entry.getValue());
+ moveRename(entry.getKey(), entry.getValue());
count++;
} catch (Exception e) {
Logger.getLogger(HistoryDialog.class.getName()).log(Level.SEVERE, e.getMessage(), e);
@@ -528,7 +528,7 @@ class HistoryDialog extends JDialog {
parent().repaint();
}
-
+
private Map getRenameMap(File directory) {
Map renameMap = new LinkedHashMap();
@@ -552,7 +552,7 @@ class HistoryDialog extends JDialog {
return renameMap;
}
-
+
private List getMissingFiles(File directory) {
List missingFiles = new ArrayList();
@@ -565,7 +565,7 @@ class HistoryDialog extends JDialog {
}
}
-
+
private final FileTransferablePolicy importHandler = new FileTransferablePolicy() {
@Override
@@ -573,13 +573,13 @@ class HistoryDialog extends JDialog {
return FileUtilities.containsOnly(files, new ExtensionFileFilter("xml"));
}
-
+
@Override
protected void clear() {
setModel(new History());
}
-
+
@Override
protected void load(List files) throws IOException {
History history = getModel();
@@ -594,7 +594,7 @@ class HistoryDialog extends JDialog {
}
}
-
+
@Override
public String getFileFilterDescription() {
return "history files (.xml)";
@@ -609,30 +609,30 @@ class HistoryDialog extends JDialog {
return true;
}
-
+
@Override
public void export(File file) throws IOException {
History.exportHistory(getModel(), file);
}
-
+
@Override
public String getDefaultFileName() {
return "history.xml";
}
};
-
+
private static class HistoryFilter extends RowFilter