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 { private final Pattern filter; - + public HistoryFilter(String filter) { this.filter = compile(quote(filter), CASE_INSENSITIVE | UNICODE_CASE | CANON_EQ); } - + @Override public boolean include(Entry entry) { // sequence model @@ -658,23 +658,23 @@ class HistoryDialog extends JDialog { throw new IllegalArgumentException("Illegal model: " + entry.getModel()); } - + private boolean include(Element element) { return include(element.to()) || include(element.from()) || include(element.dir().getPath()); } - + private boolean include(String value) { return filter.matcher(value).find(); } } - + private static class SequenceTableModel extends AbstractTableModel { private List data = emptyList(); - + public void setData(List data) { this.data = new ArrayList(data); @@ -682,12 +682,12 @@ class HistoryDialog extends JDialog { fireTableDataChanged(); } - + public List getData() { return unmodifiableList(data); } - + @Override public String getColumnName(int column) { switch (column) { @@ -704,19 +704,19 @@ class HistoryDialog extends JDialog { } } - + @Override public int getColumnCount() { return 4; } - + @Override public int getRowCount() { return data.size(); } - + @Override public Class getColumnClass(int column) { switch (column) { @@ -733,7 +733,7 @@ class HistoryDialog extends JDialog { } } - + @Override public Object getValueAt(int row, int column) { switch (column) { @@ -750,12 +750,12 @@ class HistoryDialog extends JDialog { } } - + public Sequence getRow(int row) { return data.get(row); } - + private String getName(Sequence sequence) { StringBuilder sb = new StringBuilder(); @@ -775,12 +775,12 @@ class HistoryDialog extends JDialog { } } - + private static class ElementTableModel extends AbstractTableModel { private List data = emptyList(); - + public void setData(List data) { this.data = new ArrayList(data); @@ -788,7 +788,7 @@ class HistoryDialog extends JDialog { fireTableDataChanged(); } - + @Override public String getColumnName(int column) { switch (column) { @@ -805,19 +805,19 @@ class HistoryDialog extends JDialog { } } - + @Override public int getColumnCount() { return 4; } - + @Override public int getRowCount() { return data.size(); } - + @Override public Class getColumnClass(int column) { switch (column) { @@ -834,7 +834,7 @@ class HistoryDialog extends JDialog { } } - + @Override public Object getValueAt(int row, int column) { switch (column) { @@ -851,12 +851,12 @@ class HistoryDialog extends JDialog { } } - + public Element getRow(int row) { return data.get(row); } - + public boolean isBroken(int row) { Element element = data.get(row); diff --git a/source/net/sourceforge/filebot/ui/rename/MovieHashMatcher.java b/source/net/sourceforge/filebot/ui/rename/MovieHashMatcher.java index d0dee818..35919132 100644 --- a/source/net/sourceforge/filebot/ui/rename/MovieHashMatcher.java +++ b/source/net/sourceforge/filebot/ui/rename/MovieHashMatcher.java @@ -2,8 +2,6 @@ package net.sourceforge.filebot.ui.rename; -import static java.util.Arrays.*; -import static java.util.Collections.*; import static net.sourceforge.filebot.MediaTypes.*; import static net.sourceforge.filebot.media.MediaDetection.*; import static net.sourceforge.tuned.FileUtilities.*; @@ -37,6 +35,7 @@ import javax.swing.Action; import javax.swing.SwingUtilities; import net.sourceforge.filebot.Analytics; +import net.sourceforge.filebot.media.ReleaseInfo; import net.sourceforge.filebot.similarity.Match; import net.sourceforge.filebot.similarity.NameSimilarityMetric; import net.sourceforge.filebot.similarity.SimilarityMetric; @@ -59,24 +58,30 @@ class MovieHashMatcher implements AutoCompleteMatcher { @Override public List> match(final List files, final Locale locale, final boolean autodetect, final Component parent) throws Exception { // handle movie files - File[] movieFiles = filter(files, VIDEO_FILES).toArray(new File[0]); - File[] subtitleFiles = filter(files, SUBTITLE_FILES).toArray(new File[0]); - Movie[] movieByFileHash = null; + List movieFiles = filter(files, VIDEO_FILES); - if (movieFiles.length > 0) { - // match movie hashes online - try { - movieByFileHash = service.getMovieDescriptors(movieFiles, locale); - Analytics.trackEvent(service.getName(), "HashLookup", "Movie", movieByFileHash.length - frequency(asList(movieByFileHash), null)); // number of positive hash lookups - } catch (UnsupportedOperationException e) { - movieByFileHash = new Movie[movieFiles.length]; - } - } else if (subtitleFiles.length > 0) { - // special handling if there is only subtitle files - movieByFileHash = new Movie[subtitleFiles.length]; - movieFiles = subtitleFiles; - subtitleFiles = new File[0]; + 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>(); @@ -84,10 +89,21 @@ class MovieHashMatcher implements AutoCompleteMatcher { // match remaining movies file by file in parallel List>> grabMovieJobs = new ArrayList>>(); + // match movie hashes online + Map movieByFile = new HashMap(); + if (movieFiles.size() > 0) { + try { + 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) { + // ignore + } + } + // map all files by movie - for (int i = 0; i < movieFiles.length; i++) { - final Movie movie = movieByFileHash[i]; - final File file = movieFiles[i]; + for (final File file : movieMatchFiles) { + final Movie movie = movieByFile.get(file); grabMovieJobs.add(new Callable>() { @Override @@ -146,17 +162,13 @@ class MovieHashMatcher implements AutoCompleteMatcher { } 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)); + } } } } diff --git a/source/net/sourceforge/filebot/ui/rename/RenameAction.java b/source/net/sourceforge/filebot/ui/rename/RenameAction.java index 5fc5e1d3..f1228113 100644 --- a/source/net/sourceforge/filebot/ui/rename/RenameAction.java +++ b/source/net/sourceforge/filebot/ui/rename/RenameAction.java @@ -13,16 +13,16 @@ import java.awt.event.ActionEvent; import java.beans.PropertyChangeEvent; import java.io.File; import java.util.AbstractList; +import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; -import java.util.AbstractMap.SimpleEntry; -import java.util.Map.Entry; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.logging.Level; @@ -34,15 +34,15 @@ import javax.swing.SwingWorker; import net.sourceforge.filebot.Analytics; import net.sourceforge.filebot.ResourceManager; import net.sourceforge.tuned.ui.ProgressDialog; -import net.sourceforge.tuned.ui.SwingWorkerPropertyChangeAdapter; import net.sourceforge.tuned.ui.ProgressDialog.Cancellable; +import net.sourceforge.tuned.ui.SwingWorkerPropertyChangeAdapter; class RenameAction extends AbstractAction { private final RenameModel model; - + public RenameAction(RenameModel model) { this.model = model; @@ -51,7 +51,7 @@ class RenameAction extends AbstractAction { putValue(SHORT_DESCRIPTION, "Rename files"); } - + public void actionPerformed(ActionEvent evt) { if (model.getRenameMap().isEmpty()) { return; @@ -85,7 +85,7 @@ class RenameAction extends AbstractAction { window.setCursor(Cursor.getDefaultCursor()); } - + private Map checkRenamePlan(List> renamePlan) { // build rename map and perform some sanity checks Map renameMap = new HashMap(); @@ -117,7 +117,7 @@ class RenameAction extends AbstractAction { return renameMap; } - + private List> validate(Map renameMap, Window parent) { final List> source = new ArrayList>(renameMap.size()); @@ -132,13 +132,13 @@ class RenameAction extends AbstractAction { return source.get(index).getValue(); } - + @Override public File set(int index, File name) { return source.get(index).setValue(name); } - + @Override public int size() { return source.size(); @@ -154,7 +154,7 @@ class RenameAction extends AbstractAction { return emptyList(); } - + protected ProgressDialog createProgressDialog(Window parent, final RenameJob job) { final ProgressDialog dialog = new ProgressDialog(parent, job); @@ -175,7 +175,7 @@ class RenameAction extends AbstractAction { } } - + @Override protected void done(PropertyChangeEvent evt) { dialog.close(); @@ -185,19 +185,19 @@ class RenameAction extends AbstractAction { return dialog; } - + protected class RenameJob extends SwingWorker, Void> implements Cancellable { private final Map renameMap; private final Map renameLog; - + public RenameJob(Map renameMap) { this.renameMap = synchronizedMap(renameMap); this.renameLog = synchronizedMap(new LinkedHashMap()); } - + @Override protected Map doInBackground() throws Exception { for (Entry mapping : renameMap.entrySet()) { @@ -209,7 +209,7 @@ class RenameAction extends AbstractAction { firePropertyChange("currentFile", mapping.getKey(), mapping.getValue()); // rename file, throw exception on failure - renameFile(mapping.getKey(), mapping.getValue()); + moveRename(mapping.getKey(), mapping.getValue()); // remember successfully renamed matches for history entry and possible revert renameLog.put(mapping.getKey(), mapping.getValue()); @@ -218,7 +218,7 @@ class RenameAction extends AbstractAction { return renameLog; } - + @Override protected void done() { try { @@ -251,7 +251,7 @@ class RenameAction extends AbstractAction { } } - + @Override public boolean cancel() { return cancel(true); diff --git a/source/net/sourceforge/filebot/web/IMDbClient.java b/source/net/sourceforge/filebot/web/IMDbClient.java index 21f535c5..3e00fedc 100644 --- a/source/net/sourceforge/filebot/web/IMDbClient.java +++ b/source/net/sourceforge/filebot/web/IMDbClient.java @@ -10,8 +10,10 @@ import java.io.IOException; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Scanner; import java.util.logging.Level; import java.util.logging.Logger; @@ -122,7 +124,7 @@ public class IMDbClient implements MovieIdentificationService { @Override - public Movie[] getMovieDescriptors(File[] movieFiles, Locale locale) throws Exception { + public Map getMovieDescriptors(Collection movieFiles, Locale locale) throws Exception { throw new UnsupportedOperationException(); } diff --git a/source/net/sourceforge/filebot/web/MovieIdentificationService.java b/source/net/sourceforge/filebot/web/MovieIdentificationService.java index 36695a07..cd49dd65 100644 --- a/source/net/sourceforge/filebot/web/MovieIdentificationService.java +++ b/source/net/sourceforge/filebot/web/MovieIdentificationService.java @@ -3,8 +3,10 @@ package net.sourceforge.filebot.web; import java.io.File; +import java.util.Collection; import java.util.List; import java.util.Locale; +import java.util.Map; import javax.swing.Icon; @@ -13,16 +15,16 @@ public interface MovieIdentificationService { public String getName(); - + public Icon getIcon(); - + public List searchMovie(String query, Locale locale) throws Exception; - + public Movie getMovieDescriptor(int imdbid, Locale locale) throws Exception; - - public Movie[] getMovieDescriptors(File[] movieFiles, Locale locale) throws Exception; + + public Map getMovieDescriptors(Collection movieFiles, Locale locale) throws Exception; } diff --git a/source/net/sourceforge/filebot/web/OpenSubtitlesClient.java b/source/net/sourceforge/filebot/web/OpenSubtitlesClient.java index cc4d3540..49565d90 100644 --- a/source/net/sourceforge/filebot/web/OpenSubtitlesClient.java +++ b/source/net/sourceforge/filebot/web/OpenSubtitlesClient.java @@ -4,6 +4,7 @@ package net.sourceforge.filebot.web; import static java.lang.Math.*; import static java.util.Arrays.*; +import static java.util.Collections.*; import static net.sourceforge.filebot.web.OpenSubtitlesHasher.*; import java.io.File; @@ -12,6 +13,7 @@ import java.net.URI; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -184,37 +186,36 @@ public class OpenSubtitlesClient implements SubtitleProvider, VideoHashSubtitleS public Movie getMovieDescriptor(File movieFile, Locale locale) throws Exception { - return getMovieDescriptors(new File[] { movieFile }, locale)[0]; + return getMovieDescriptors(singleton(movieFile), locale).get(movieFile); } @Override - public Movie[] getMovieDescriptors(File[] movieFiles, Locale locale) throws Exception { + public Map getMovieDescriptors(Collection movieFiles, Locale locale) throws Exception { // create result array - Movie[] result = new Movie[movieFiles.length]; + Map result = new HashMap(); // compute movie hashes - Map indexMap = new HashMap(movieFiles.length); + Map hashMap = new HashMap(movieFiles.size()); - for (int i = 0; i < movieFiles.length; i++) { - if (movieFiles[i].length() > HASH_CHUNK_SIZE) { - indexMap.put(computeHash(movieFiles[i]), i); // remember original index + for (File file : movieFiles) { + if (file.length() > HASH_CHUNK_SIZE) { + hashMap.put(computeHash(file), file); // map file by hash } } - if (indexMap.size() > 0) { + if (hashMap.size() > 0) { // require login login(); // dispatch query for all hashes - List hashes = new ArrayList(indexMap.keySet()); + List hashes = new ArrayList(hashMap.keySet()); int batchSize = 50; for (int bn = 0; bn < ceil((float) hashes.size() / batchSize); bn++) { List batch = hashes.subList(bn * batchSize, min((bn * batchSize) + batchSize, hashes.size())); for (Entry entry : xmlrpc.checkMovieHash(batch).entrySet()) { - int index = indexMap.get(entry.getKey()); - result[index] = entry.getValue(); + result.put(hashMap.get(entry.getKey()), entry.getValue()); } } } diff --git a/source/net/sourceforge/filebot/web/TMDbClient.java b/source/net/sourceforge/filebot/web/TMDbClient.java index f09a861c..08074088 100644 --- a/source/net/sourceforge/filebot/web/TMDbClient.java +++ b/source/net/sourceforge/filebot/web/TMDbClient.java @@ -13,6 +13,7 @@ import java.io.Serializable; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; +import java.util.Collection; import java.util.EnumMap; import java.util.List; import java.util.Locale; @@ -70,11 +71,6 @@ public class TMDbClient implements MovieIdentificationService { } - public List searchMovie(File file, Locale locale) throws IOException, SAXException { - throw new UnsupportedOperationException(); - } - - public List searchMovie(String hash, long bytesize, Locale locale) throws IOException, SAXException { return getMovies("Media.getInfo", hash + "/" + bytesize, locale); } @@ -96,17 +92,8 @@ public class TMDbClient implements MovieIdentificationService { @Override - public Movie[] getMovieDescriptors(File[] movieFiles, Locale locale) throws Exception { - Movie[] movies = new Movie[movieFiles.length]; - - for (int i = 0; i < movies.length; i++) { - List options = searchMovie(movieFiles[i], locale); - - // just use first result, if possible - movies[i] = options.isEmpty() ? null : options.get(0); - } - - return movies; + public Map getMovieDescriptors(Collection movieFiles, Locale locale) throws Exception { + throw new UnsupportedOperationException(); } diff --git a/source/net/sourceforge/tuned/FastFile.java b/source/net/sourceforge/tuned/FastFile.java index 9e042249..a50fef73 100644 --- a/source/net/sourceforge/tuned/FastFile.java +++ b/source/net/sourceforge/tuned/FastFile.java @@ -13,43 +13,36 @@ public class FastFile extends File { private Long length; private Boolean isDirectory; private Boolean isFile; - private Boolean exists; public FastFile(String path) { super(path); } - + public FastFile(File parent, String child) { super(parent, child); } - + @Override public long length() { return length != null ? length : (length = super.length()); } - - @Override - public boolean exists() { - return exists != null ? exists : (exists = super.exists()); - } - @Override public boolean isDirectory() { return isDirectory != null ? isDirectory : (isDirectory = super.isDirectory()); } - + @Override public boolean isFile() { return isFile != null ? isFile : (isFile = super.isFile()); } - + @Override public File[] listFiles() { String[] names = list(); @@ -62,12 +55,12 @@ public class FastFile extends File { return files; } - + public static List foreach(File... files) { return foreach(Arrays.asList(files)); } - + public static List foreach(final List files) { List result = new ArrayList(files.size()); diff --git a/source/net/sourceforge/tuned/FileUtilities.java b/source/net/sourceforge/tuned/FileUtilities.java index e4c6e9f6..617bf5fc 100644 --- a/source/net/sourceforge/tuned/FileUtilities.java +++ b/source/net/sourceforge/tuned/FileUtilities.java @@ -42,7 +42,7 @@ import com.ibm.icu.text.CharsetMatch; public final class FileUtilities { - public static File renameFile(File source, File destination) throws IOException { + public static File moveRename(File source, File destination) throws IOException { // resolve destination if (!destination.isAbsolute()) { // same folder, different name @@ -57,25 +57,37 @@ public final class FileUtilities { throw new IOException("Failed to create folder: " + destinationFolder); } - try { - renameFileNIO2(source, destination); - } catch (LinkageError e) { - renameFileIO(source, destination); + if (source.isDirectory()) { // move folder + moveFolderIO(source, destination); + } else { // move file + try { + moveFileNIO2(source, destination); + } catch (LinkageError e) { + moveFileIO(source, destination); + } } return destination; } - private static void renameFileNIO2(File source, File destination) throws IOException { + private static void moveFileNIO2(File source, File destination) throws IOException { java.nio.file.Files.move(source.toPath(), destination.toPath()); } - private static void renameFileIO(File source, File destination) throws IOException { + private static void moveFileIO(File source, File destination) throws IOException { if (!source.renameTo(destination)) { - // try using Guava IO utilities, that'll just copy files if renameTo() fails - com.google.common.io.Files.move(source, destination); + // use "copy and delete" as fallback if standard rename fails + org.apache.commons.io.FileUtils.moveFile(source, destination); + } + } + + + private static void moveFolderIO(File source, File destination) throws IOException { + if (!source.renameTo(destination)) { + // use "copy and delete" as fallback if standard move/rename fails + org.apache.commons.io.FileUtils.moveDirectory(source, destination); } }