mirror of
https://github.com/mitb-archive/filebot
synced 2024-11-04 16:35:08 -05:00
+ efficient support for mass-renaming of lots of files in lots of folders
This commit is contained in:
parent
2bf426dedd
commit
d125c4dd1a
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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) {
|
||||
|
@ -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>>();
|
||||
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user