+ efficient support for mass-renaming of lots of files in lots of folders

This commit is contained in:
Reinhard Pointner 2011-11-26 09:50:31 +00:00
parent 2bf426dedd
commit d125c4dd1a
7 changed files with 159 additions and 34 deletions

View File

@ -49,28 +49,42 @@ public class ReleaseInfo {
}
public List<String> clean(Iterable<String> items) {
public List<String> clean(Iterable<String> items) throws IOException {
return clean(items, getVideoSourcePattern(), getCodecPattern());
}
public String clean(String item) throws IOException {
return clean(item, getVideoSourcePattern(), getCodecPattern());
}
public List<String> cleanRG(Iterable<String> items) throws IOException {
return clean(items, getReleaseGroupPattern(), getVideoSourcePattern(), getCodecPattern());
}
public String cleanRG(String item) throws IOException {
return clean(item, getReleaseGroupPattern(), getVideoSourcePattern(), getCodecPattern());
}
public List<String> clean(Iterable<String> items, Pattern... blacklisted) {
List<String> cleaned = new ArrayList<String>();
for (String string : items) {
for (Pattern it : blacklisted) {
string = it.matcher(string).replaceAll("");
}
cleaned.add(string.replaceAll("[\\p{Punct}\\p{Space}]+", " ").trim());
List<String> cleanedItems = new ArrayList<String>();
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();
}

View File

@ -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<File, ?>> match(List<File> files, Locale locale, boolean autodetection) throws Exception;
List<Match<File, ?>> match(List<File> files, Locale locale, boolean autodetection, Window parent) throws Exception;
}

View File

@ -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<SearchResult> searchResults) throws Exception {
protected SearchResult selectSearchResult(final String query, final List<SearchResult> 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<SearchResult> selectDialog = new SelectDialog<SearchResult>(null, searchResults);
SelectDialog<SearchResult> selectDialog = new SelectDialog<SearchResult>(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<Episode> fetchEpisodeSet(Collection<String> seriesNames, final Locale locale) throws Exception {
protected Collection<String> detectSeriesNames(Collection<File> files) {
return new SeriesNameMatcher().matchAll(files.toArray(new File[0]));
}
protected Set<Episode> fetchEpisodeSet(Collection<String> seriesNames, final Locale locale, final Window window) throws Exception {
List<Callable<List<Episode>>> tasks = new ArrayList<Callable<List<Episode>>>();
// 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<Episode> episodes = provider.getEpisodeList(selectedSearchResult, locale);
@ -164,27 +171,83 @@ class EpisodeListMatcher implements AutoCompleteMatcher {
@Override
public List<Match<File, ?>> match(final List<File> files, Locale locale, boolean autodetection) throws Exception {
public List<Match<File, ?>> match(final List<File> files, final Locale locale, final boolean autodetection, final Window window) throws Exception {
// focus on movie and subtitle files
List<File> mediaFiles = FileUtilities.filter(files, VIDEO_FILES, SUBTITLE_FILES);
final List<File> mediaFiles = FileUtilities.filter(files, VIDEO_FILES, SUBTITLE_FILES);
final Map<File, List<File>> 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<Callable<List<Match<File, ?>>>> taskPerFolder = new ArrayList<Callable<List<Match<File, ?>>>>();
// detect series names and create episode list fetch tasks
for (final List<File> folder : filesByFolder.values()) {
taskPerFolder.add(new Callable<List<Match<File, ?>>>() {
@Override
public List<Match<File, ?>> 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<Match<File, ?>> matches = new ArrayList<Match<File, ?>>();
for (Future<List<Match<File, ?>>> future : executor.invokeAll(taskPerFolder)) {
matches.addAll(future.get());
}
// all background workers have finished
return matches;
} finally {
// destroy background threads
executor.shutdownNow();
}
}
public List<Match<File, ?>> matchEpisodeSet(final List<File> files, Locale locale, boolean autodetection, Window window) throws Exception {
Set<Episode> episodes = emptySet();
// detect series name and fetch episode list
if (autodetection) {
Collection<String> names = new SeriesNameMatcher().matchAll(files.toArray(new File[0]));
Collection<String> 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<Match<File, ?>> matches = new ArrayList<Match<File, ?>>();
// group by subtitles first and then by files in general
for (List<File> filesPerType : mapByExtension(mediaFiles).values()) {
for (List<File> filesPerType : mapByExtension(files).values()) {
Matcher<File, Episode> matcher = new Matcher<File, Episode>(filesPerType, episodes, false, EpisodeMetrics.defaultSequence(false));
matches.addAll(matcher.match());
}
@ -208,4 +271,5 @@ class EpisodeListMatcher implements AutoCompleteMatcher {
return matches;
}
}

View File

@ -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<File, ?>> match(final List<File> files, Locale locale, boolean autodetect) throws Exception {
public List<Match<File, ?>> match(final List<File> 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<Movie> options = new ArrayList<Movie>();
// 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<Movie> options) throws Exception {
protected Movie selectMovie(final List<Movie> 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<Movie> selectDialog = new SelectDialog<Movie>(null, options);
SelectDialog<Movie> selectDialog = new SelectDialog<Movie>(window, options);
selectDialog.getHeaderLabel().setText("Select Movie:");
selectDialog.getCancelAction().putValue(Action.NAME, "Ignore");

View File

@ -355,7 +355,7 @@ public class RenamePanel extends JComponent {
@Override
protected List<Match<File, ?>> doInBackground() throws Exception {
List<Match<File, ?>> matches = matcher.match(remainingFiles, locale, autodetection);
List<Match<File, ?>> matches = matcher.match(remainingFiles, locale, autodetection, getWindow(RenamePanel.this));
// remove matched files
for (Match<File, ?> match : matches) {

View File

@ -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<File, List<File>> mapByFolder(Iterable<File> files) {
SortedMap<File, List<File>> map = new TreeMap<File, List<File>>();
for (File file : files) {
List<File> valueList = map.get(file.getParentFile());
if (valueList == null) {
valueList = new ArrayList<File>();
map.put(file.getParentFile(), valueList);
}
valueList.add(file);
}
return map;
}
public static Map<String, List<File>> mapByExtension(Iterable<File> files) {
Map<String, List<File>> map = new HashMap<String, List<File>>();

View File

@ -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;