From d125c4dd1aea0834b7a1040dc383a9c0c1ffbb32 Mon Sep 17 00:00:00 2001 From: Reinhard Pointner Date: Sat, 26 Nov 2011 09:50:31 +0000 Subject: [PATCH] + efficient support for mass-renaming of lots of files in lots of folders --- .../filebot/mediainfo/ReleaseInfo.java | 34 +++++-- .../ui/rename/AutoCompleteMatcher.java | 3 +- .../filebot/ui/rename/EpisodeListMatcher.java | 92 ++++++++++++++++--- .../filebot/ui/rename/MovieHashMatcher.java | 20 ++-- .../filebot/ui/rename/RenamePanel.java | 2 +- .../net/sourceforge/tuned/FileUtilities.java | 20 ++++ .../sourceforge/tuned/ui/TunedUtilities.java | 22 +++++ 7 files changed, 159 insertions(+), 34 deletions(-) diff --git a/source/net/sourceforge/filebot/mediainfo/ReleaseInfo.java b/source/net/sourceforge/filebot/mediainfo/ReleaseInfo.java index 2867fadf..fb5f6896 100644 --- a/source/net/sourceforge/filebot/mediainfo/ReleaseInfo.java +++ b/source/net/sourceforge/filebot/mediainfo/ReleaseInfo.java @@ -49,28 +49,42 @@ public class ReleaseInfo { } - public List clean(Iterable items) { + public List clean(Iterable items) throws IOException { return clean(items, getVideoSourcePattern(), getCodecPattern()); } + public String clean(String item) throws IOException { + return clean(item, getVideoSourcePattern(), getCodecPattern()); + } + + public List cleanRG(Iterable items) throws IOException { return clean(items, getReleaseGroupPattern(), getVideoSourcePattern(), getCodecPattern()); } + public String cleanRG(String item) throws IOException { + return clean(item, getReleaseGroupPattern(), getVideoSourcePattern(), getCodecPattern()); + } + + public List clean(Iterable items, Pattern... blacklisted) { - List cleaned = new ArrayList(); - - for (String string : items) { - for (Pattern it : blacklisted) { - string = it.matcher(string).replaceAll(""); - } - - cleaned.add(string.replaceAll("[\\p{Punct}\\p{Space}]+", " ").trim()); + List cleanedItems = new ArrayList(); + for (String it : items) { + cleanedItems.add(clean(it, blacklisted)); } - return cleaned; + return cleanedItems; + } + + + public String clean(String item, Pattern... blacklisted) { + for (Pattern it : blacklisted) { + item = it.matcher(item).replaceAll(""); + } + + return item.replaceAll("[\\p{Punct}\\p{Space}]+", " ").trim(); } diff --git a/source/net/sourceforge/filebot/ui/rename/AutoCompleteMatcher.java b/source/net/sourceforge/filebot/ui/rename/AutoCompleteMatcher.java index 4b728ef7..1bf54cbf 100644 --- a/source/net/sourceforge/filebot/ui/rename/AutoCompleteMatcher.java +++ b/source/net/sourceforge/filebot/ui/rename/AutoCompleteMatcher.java @@ -2,6 +2,7 @@ package net.sourceforge.filebot.ui.rename; +import java.awt.Window; import java.io.File; import java.util.List; import java.util.Locale; @@ -11,5 +12,5 @@ import net.sourceforge.filebot.similarity.Match; interface AutoCompleteMatcher { - List> match(List files, Locale locale, boolean autodetection) throws Exception; + List> match(List files, Locale locale, boolean autodetection, Window parent) throws Exception; } diff --git a/source/net/sourceforge/filebot/ui/rename/EpisodeListMatcher.java b/source/net/sourceforge/filebot/ui/rename/EpisodeListMatcher.java index ca48642c..bdb584bb 100644 --- a/source/net/sourceforge/filebot/ui/rename/EpisodeListMatcher.java +++ b/source/net/sourceforge/filebot/ui/rename/EpisodeListMatcher.java @@ -3,13 +3,13 @@ package net.sourceforge.filebot.ui.rename; import static java.util.Collections.*; -import static javax.swing.JOptionPane.*; import static net.sourceforge.filebot.MediaTypes.*; import static net.sourceforge.filebot.web.EpisodeUtilities.*; import static net.sourceforge.tuned.FileUtilities.*; import static net.sourceforge.tuned.ui.TunedUtilities.*; import java.awt.Dimension; +import java.awt.Window; import java.io.File; import java.util.ArrayList; import java.util.Collection; @@ -19,6 +19,7 @@ import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; @@ -31,6 +32,7 @@ import javax.swing.Action; import javax.swing.SwingUtilities; import net.sourceforge.filebot.Analytics; +import net.sourceforge.filebot.mediainfo.ReleaseInfo; import net.sourceforge.filebot.similarity.EpisodeMetrics; import net.sourceforge.filebot.similarity.Match; import net.sourceforge.filebot.similarity.Matcher; @@ -54,7 +56,7 @@ class EpisodeListMatcher implements AutoCompleteMatcher { } - protected SearchResult selectSearchResult(final String query, final List searchResults) throws Exception { + protected SearchResult selectSearchResult(final String query, final List searchResults, final Window window) throws Exception { if (searchResults.size() == 1) { return searchResults.get(0); } @@ -83,7 +85,7 @@ class EpisodeListMatcher implements AutoCompleteMatcher { @Override public SearchResult call() throws Exception { // multiple results have been found, user must select one - SelectDialog selectDialog = new SelectDialog(null, searchResults); + SelectDialog selectDialog = new SelectDialog(window, searchResults); selectDialog.getHeaderLabel().setText(String.format("Shows matching '%s':", query)); selectDialog.getCancelAction().putValue(Action.NAME, "Ignore"); @@ -115,7 +117,12 @@ class EpisodeListMatcher implements AutoCompleteMatcher { } - protected Set fetchEpisodeSet(Collection seriesNames, final Locale locale) throws Exception { + protected Collection detectSeriesNames(Collection files) { + return new SeriesNameMatcher().matchAll(files.toArray(new File[0])); + } + + + protected Set fetchEpisodeSet(Collection seriesNames, final Locale locale, final Window window) throws Exception { List>> tasks = new ArrayList>>(); // detect series names and create episode list fetch tasks @@ -128,7 +135,7 @@ class EpisodeListMatcher implements AutoCompleteMatcher { // select search result if (results.size() > 0) { - SearchResult selectedSearchResult = selectSearchResult(query, results); + SearchResult selectedSearchResult = selectSearchResult(query, results, window); if (selectedSearchResult != null) { List episodes = provider.getEpisodeList(selectedSearchResult, locale); @@ -164,27 +171,83 @@ class EpisodeListMatcher implements AutoCompleteMatcher { @Override - public List> match(final List files, Locale locale, boolean autodetection) throws Exception { + public List> match(final List files, final Locale locale, final boolean autodetection, final Window window) throws Exception { // focus on movie and subtitle files - List mediaFiles = FileUtilities.filter(files, VIDEO_FILES, SUBTITLE_FILES); + final List mediaFiles = FileUtilities.filter(files, VIDEO_FILES, SUBTITLE_FILES); + final Map> filesByFolder = mapByFolder(mediaFiles); + + // do matching all at once + if (filesByFolder.keySet().size() <= 5 || detectSeriesNames(mediaFiles).size() <= 5) { + return matchEpisodeSet(mediaFiles, locale, autodetection, window); + } + + // assume that many shows will be matched, do it folder by folder + List>>> taskPerFolder = new ArrayList>>>(); + + // detect series names and create episode list fetch tasks + for (final List folder : filesByFolder.values()) { + taskPerFolder.add(new Callable>>() { + + @Override + public List> call() throws Exception { + return matchEpisodeSet(folder, locale, autodetection, window); + } + }); + } + + // match folder per folder in parallel + ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); + + try { + // merge all episodes + List> matches = new ArrayList>(); + for (Future>> future : executor.invokeAll(taskPerFolder)) { + matches.addAll(future.get()); + } + + // all background workers have finished + return matches; + } finally { + // destroy background threads + executor.shutdownNow(); + } + } + + + public List> matchEpisodeSet(final List files, Locale locale, boolean autodetection, Window window) throws Exception { Set episodes = emptySet(); // detect series name and fetch episode list if (autodetection) { - Collection names = new SeriesNameMatcher().matchAll(files.toArray(new File[0])); - + Collection names = detectSeriesNames(files); if (names.size() > 0) { - episodes = fetchEpisodeSet(names, locale); + // only allow one fetch session at a time so later requests can make use of cached results + synchronized (provider) { + episodes = fetchEpisodeSet(names, locale, window); + } } } // require user input if auto-detection has failed or has been disabled if (episodes.isEmpty()) { - String suggestion = new SeriesNameMatcher().matchBySeasonEpisodePattern(getName(files.iterator().next())); - String input = showInputDialog(null, "Enter series name:", suggestion); + String suggestion = new SeriesNameMatcher().matchBySeasonEpisodePattern(getName(files.get(0))); + if (suggestion == null) { + suggestion = files.get(0).getParentFile().getName(); + } + + // clean media info / release group info / etc + suggestion = new ReleaseInfo().cleanRG(suggestion); + + String input = null; + synchronized (this) { + input = showInputDialog("Enter series name:", suggestion, files.get(0).getParentFile().getName(), window); + } if (input != null) { - episodes = fetchEpisodeSet(singleton(input), locale); + // only allow one fetch session at a time so later requests can make use of cached results + synchronized (provider) { + episodes = fetchEpisodeSet(singleton(input), locale, window); + } } } @@ -192,7 +255,7 @@ class EpisodeListMatcher implements AutoCompleteMatcher { List> matches = new ArrayList>(); // group by subtitles first and then by files in general - for (List filesPerType : mapByExtension(mediaFiles).values()) { + for (List filesPerType : mapByExtension(files).values()) { Matcher matcher = new Matcher(filesPerType, episodes, false, EpisodeMetrics.defaultSequence(false)); matches.addAll(matcher.match()); } @@ -208,4 +271,5 @@ class EpisodeListMatcher implements AutoCompleteMatcher { return matches; } + } diff --git a/source/net/sourceforge/filebot/ui/rename/MovieHashMatcher.java b/source/net/sourceforge/filebot/ui/rename/MovieHashMatcher.java index 8d14768c..5785e919 100644 --- a/source/net/sourceforge/filebot/ui/rename/MovieHashMatcher.java +++ b/source/net/sourceforge/filebot/ui/rename/MovieHashMatcher.java @@ -4,11 +4,11 @@ package net.sourceforge.filebot.ui.rename; import static java.util.Arrays.*; import static java.util.Collections.*; -import static javax.swing.JOptionPane.*; import static net.sourceforge.filebot.MediaTypes.*; import static net.sourceforge.tuned.FileUtilities.*; import static net.sourceforge.tuned.ui.TunedUtilities.*; +import java.awt.Window; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -53,7 +53,7 @@ class MovieHashMatcher implements AutoCompleteMatcher { @Override - public List> match(final List files, Locale locale, boolean autodetect) throws Exception { + public List> match(final List files, Locale locale, boolean autodetect, Window window) throws Exception { // handle movie files File[] movieFiles = filter(files, VIDEO_FILES).toArray(new File[0]); @@ -70,7 +70,7 @@ class MovieHashMatcher implements AutoCompleteMatcher { // unknown hash, try via imdb id from nfo file if (movie == null || !autodetect) { - movie = grabMovieName(movieFiles[i], locale, autodetect, movie); + movie = grabMovieName(movieFiles[i], locale, autodetect, window, movie); if (movie != null) { Analytics.trackEvent(service.getName(), "SearchMovie", movie.toString(), 1); @@ -161,7 +161,7 @@ class MovieHashMatcher implements AutoCompleteMatcher { } - protected Movie grabMovieName(File movieFile, Locale locale, boolean autodetect, Movie... suggestions) throws Exception { + protected Movie grabMovieName(File movieFile, Locale locale, boolean autodetect, Window window, Movie... suggestions) throws Exception { List options = new ArrayList(); // add default value if any @@ -197,7 +197,11 @@ class MovieHashMatcher implements AutoCompleteMatcher { // allow manual user input if (options.isEmpty() || !autodetect) { String suggestion = options.isEmpty() ? searchQueries.iterator().next() : options.get(0).getName(); - String input = showInputDialog(null, "Enter movie name:", suggestion); + + String input = null; + synchronized (this) { + input = showInputDialog("Enter movie name:", suggestion, options.get(0).getName(), window); + } if (input != null) { options = service.searchMovie(input, locale); @@ -206,11 +210,11 @@ class MovieHashMatcher implements AutoCompleteMatcher { } } - return options.isEmpty() ? null : selectMovie(options); + return options.isEmpty() ? null : selectMovie(options, window); } - protected Movie selectMovie(final List options) throws Exception { + protected Movie selectMovie(final List options, final Window window) throws Exception { if (options.size() == 1) { return options.get(0); } @@ -221,7 +225,7 @@ class MovieHashMatcher implements AutoCompleteMatcher { @Override public Movie call() throws Exception { // multiple results have been found, user must select one - SelectDialog selectDialog = new SelectDialog(null, options); + SelectDialog selectDialog = new SelectDialog(window, options); selectDialog.getHeaderLabel().setText("Select Movie:"); selectDialog.getCancelAction().putValue(Action.NAME, "Ignore"); diff --git a/source/net/sourceforge/filebot/ui/rename/RenamePanel.java b/source/net/sourceforge/filebot/ui/rename/RenamePanel.java index fd623da1..7c99037c 100644 --- a/source/net/sourceforge/filebot/ui/rename/RenamePanel.java +++ b/source/net/sourceforge/filebot/ui/rename/RenamePanel.java @@ -355,7 +355,7 @@ public class RenamePanel extends JComponent { @Override protected List> doInBackground() throws Exception { - List> matches = matcher.match(remainingFiles, locale, autodetection); + List> matches = matcher.match(remainingFiles, locale, autodetection, getWindow(RenamePanel.this)); // remove matched files for (Match match : matches) { diff --git a/source/net/sourceforge/tuned/FileUtilities.java b/source/net/sourceforge/tuned/FileUtilities.java index 398f96cd..e39986c2 100644 --- a/source/net/sourceforge/tuned/FileUtilities.java +++ b/source/net/sourceforge/tuned/FileUtilities.java @@ -22,6 +22,8 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -340,6 +342,24 @@ public final class FileUtilities { } + public static SortedMap> mapByFolder(Iterable files) { + SortedMap> map = new TreeMap>(); + + for (File file : files) { + List valueList = map.get(file.getParentFile()); + + if (valueList == null) { + valueList = new ArrayList(); + map.put(file.getParentFile(), valueList); + } + + valueList.add(file); + } + + return map; + } + + public static Map> mapByExtension(Iterable files) { Map> map = new HashMap>(); diff --git a/source/net/sourceforge/tuned/ui/TunedUtilities.java b/source/net/sourceforge/tuned/ui/TunedUtilities.java index bd81367d..ac254b4a 100644 --- a/source/net/sourceforge/tuned/ui/TunedUtilities.java +++ b/source/net/sourceforge/tuned/ui/TunedUtilities.java @@ -2,6 +2,8 @@ package net.sourceforge.tuned.ui; +import static javax.swing.JOptionPane.*; + import java.awt.Color; import java.awt.Component; import java.awt.Dimension; @@ -15,6 +17,7 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; +import java.lang.reflect.InvocationTargetException; import javax.swing.AbstractAction; import javax.swing.Action; @@ -22,6 +25,7 @@ import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JComponent; +import javax.swing.JOptionPane; import javax.swing.KeyStroke; import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; @@ -114,6 +118,24 @@ public final class TunedUtilities { } + public static String showInputDialog(final String text, final String initialValue, final String title, final Window parent) throws InvocationTargetException, InterruptedException { + final StringBuilder buffer = new StringBuilder(); + SwingUtilities.invokeAndWait(new Runnable() { + + @Override + public void run() { + Object value = JOptionPane.showInputDialog(parent, text, title, PLAIN_MESSAGE, null, null, initialValue); + + if (value != null) { + buffer.append(value); + } + } + }); + + return buffer.length() == 0 ? null : buffer.toString(); + } + + public static Window getWindow(Object component) { if (component instanceof Window) return (Window) component;