Refactor EpisodeListMatcher
This commit is contained in:
parent
d31d24856c
commit
26c49fb96b
|
@ -1,8 +1,9 @@
|
|||
package net.filebot.ui.rename;
|
||||
|
||||
import static java.util.Collections.*;
|
||||
import static java.util.Comparator.*;
|
||||
import static java.util.stream.Collectors.*;
|
||||
import static net.filebot.MediaTypes.*;
|
||||
import static net.filebot.Settings.*;
|
||||
import static net.filebot.WebServices.*;
|
||||
import static net.filebot.media.MediaDetection.*;
|
||||
import static net.filebot.similarity.Normalization.*;
|
||||
|
@ -11,29 +12,23 @@ import static net.filebot.util.StringUtilities.*;
|
|||
import static net.filebot.util.ui.SwingUI.*;
|
||||
|
||||
import java.awt.Component;
|
||||
import java.awt.Dimension;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.FutureTask;
|
||||
import java.util.concurrent.RunnableFuture;
|
||||
import java.util.prefs.Preferences;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.swing.Action;
|
||||
import javax.swing.SwingUtilities;
|
||||
|
@ -41,7 +36,6 @@ import javax.swing.SwingUtilities;
|
|||
import net.filebot.Cache;
|
||||
import net.filebot.Cache.TypedCache;
|
||||
import net.filebot.CacheType;
|
||||
import net.filebot.Settings;
|
||||
import net.filebot.similarity.CommonSequenceMatcher;
|
||||
import net.filebot.similarity.EpisodeMatcher;
|
||||
import net.filebot.similarity.Match;
|
||||
|
@ -56,19 +50,19 @@ class EpisodeListMatcher implements AutoCompleteMatcher {
|
|||
private EpisodeListProvider provider;
|
||||
private boolean anime;
|
||||
|
||||
// remember user selections
|
||||
private TypedCache<SearchResult> persistentSelectionMemory;
|
||||
|
||||
// only allow one fetch session at a time so later requests can make use of cached results
|
||||
private final Object providerLock = new Object();
|
||||
|
||||
public EpisodeListMatcher(EpisodeListProvider provider, boolean anime) {
|
||||
this.provider = provider;
|
||||
this.anime = anime;
|
||||
this.persistentSelectionMemory = Cache.getCache("selection_" + provider.getName(), CacheType.Persistent).cast(SearchResult.class);
|
||||
}
|
||||
|
||||
protected SearchResult selectSearchResult(final String query, final List<SearchResult> searchResults, Map<String, SearchResult> selectionMemory, boolean autodetection, final Component parent) throws Exception {
|
||||
public TypedCache<SearchResult> getPersistentSelectionMemory() {
|
||||
return Cache.getCache("selection_" + provider.getName(), CacheType.Persistent).cast(SearchResult.class);
|
||||
}
|
||||
|
||||
protected SearchResult selectSearchResult(List<File> files, String query, List<SearchResult> searchResults, Map<String, SearchResult> selectionMemory, boolean autodetection, Component parent) throws Exception {
|
||||
if (searchResults.size() == 1) {
|
||||
return searchResults.get(0);
|
||||
}
|
||||
|
@ -82,104 +76,80 @@ class EpisodeListMatcher implements AutoCompleteMatcher {
|
|||
}
|
||||
|
||||
// show selection dialog on EDT
|
||||
final RunnableFuture<SearchResult> showSelectDialog = new FutureTask<SearchResult>(new Callable<SearchResult>() {
|
||||
RunnableFuture<SearchResult> showSelectDialog = new FutureTask<SearchResult>(() -> {
|
||||
// multiple results have been found, user must select one
|
||||
SelectDialog<SearchResult> selectDialog = new SelectDialog<SearchResult>(parent, searchResults, true, false);
|
||||
selectDialog.setTitle(provider.getName());
|
||||
selectDialog.getHeaderLabel().setText(getQueryInputMessage(String.format("Select best match for \"<b>%s</b>\":", query), getFilesForQuery(files, query)));
|
||||
selectDialog.getCancelAction().putValue(Action.NAME, "Ignore");
|
||||
selectDialog.pack();
|
||||
|
||||
@Override
|
||||
public SearchResult call() throws Exception {
|
||||
// multiple results have been found, user must select one
|
||||
SelectDialog<SearchResult> selectDialog = new SelectDialog<SearchResult>(parent, searchResults, true, false);
|
||||
// show dialog
|
||||
selectDialog.restoreState(Preferences.userNodeForPackage(EpisodeListMatcher.class));
|
||||
selectDialog.setLocation(getOffsetLocation(selectDialog.getOwner()));
|
||||
selectDialog.setVisible(true);
|
||||
|
||||
selectDialog.getHeaderLabel().setText(String.format("Shows matching '%s':", query));
|
||||
selectDialog.getCancelAction().putValue(Action.NAME, "Ignore");
|
||||
// remember dialog state
|
||||
selectDialog.saveState(Preferences.userNodeForPackage(EpisodeListMatcher.class));
|
||||
|
||||
// restore original dialog size
|
||||
Settings prefs = Settings.forPackage(EpisodeListMatcher.class);
|
||||
int w = Integer.parseInt(prefs.get("dialog.select.w", "280"));
|
||||
int h = Integer.parseInt(prefs.get("dialog.select.h", "300"));
|
||||
selectDialog.setPreferredSize(new Dimension(w, h));
|
||||
selectDialog.pack();
|
||||
|
||||
// show dialog
|
||||
selectDialog.setLocation(getOffsetLocation(selectDialog.getOwner()));
|
||||
selectDialog.setVisible(true);
|
||||
|
||||
// remember dialog size
|
||||
prefs.put("dialog.select.w", Integer.toString(selectDialog.getWidth()));
|
||||
prefs.put("dialog.select.h", Integer.toString(selectDialog.getHeight()));
|
||||
|
||||
if (selectDialog.getSelectedAction() == null) {
|
||||
throw new CancellationException();
|
||||
}
|
||||
|
||||
// remember if we should auto-repeat the chosen action in the future
|
||||
if (selectDialog.getAutoRepeatCheckBox().isSelected() && selectDialog.getSelectedValue() != null) {
|
||||
persistentSelectionMemory.put(query, selectDialog.getSelectedValue());
|
||||
}
|
||||
|
||||
// selected value or null if the dialog was canceled by the user
|
||||
return selectDialog.getSelectedValue();
|
||||
if (selectDialog.getSelectedAction() == null) {
|
||||
throw new CancellationException();
|
||||
}
|
||||
|
||||
// remember if we should auto-repeat the chosen action in the future
|
||||
if (selectDialog.getAutoRepeatCheckBox().isSelected() && selectDialog.getSelectedValue() != null) {
|
||||
getPersistentSelectionMemory().put(query, selectDialog.getSelectedValue());
|
||||
}
|
||||
|
||||
// selected value or null if the dialog was canceled by the user
|
||||
return selectDialog.getSelectedValue();
|
||||
});
|
||||
|
||||
// allow only one select dialog at a time
|
||||
synchronized (this) {
|
||||
synchronized (selectionMemory) {
|
||||
if (selectionMemory.containsKey(query)) {
|
||||
return selectionMemory.get(query);
|
||||
}
|
||||
|
||||
// check persistent memory
|
||||
if (autodetection) {
|
||||
SearchResult persistentSelection = persistentSelectionMemory.get(query);
|
||||
if (persistentSelection != null) {
|
||||
return persistentSelection;
|
||||
}
|
||||
}
|
||||
|
||||
// ask user
|
||||
SwingUtilities.invokeAndWait(showSelectDialog);
|
||||
SearchResult userSelection = showSelectDialog.get();
|
||||
|
||||
// remember selected value
|
||||
selectionMemory.put(query, userSelection);
|
||||
return userSelection;
|
||||
synchronized (parent) {
|
||||
if (selectionMemory.containsKey(query)) {
|
||||
return selectionMemory.get(query);
|
||||
}
|
||||
|
||||
// check persistent memory
|
||||
if (autodetection) {
|
||||
SearchResult persistentSelection = getPersistentSelectionMemory().get(query);
|
||||
if (persistentSelection != null) {
|
||||
return persistentSelection;
|
||||
}
|
||||
}
|
||||
|
||||
// ask user
|
||||
SwingUtilities.invokeAndWait(showSelectDialog);
|
||||
SearchResult userSelection = showSelectDialog.get();
|
||||
|
||||
// remember selected value
|
||||
selectionMemory.put(query, userSelection);
|
||||
return userSelection;
|
||||
}
|
||||
}
|
||||
|
||||
protected Set<Episode> fetchEpisodeSet(Collection<String> seriesNames, final SortOrder sortOrder, final Locale locale, final Map<String, SearchResult> selectionMemory, final boolean autodetection, final Component parent) throws Exception {
|
||||
List<Callable<List<Episode>>> tasks = new ArrayList<Callable<List<Episode>>>();
|
||||
|
||||
// detect series names and create episode list fetch tasks
|
||||
for (final String query : seriesNames) {
|
||||
tasks.add(new Callable<List<Episode>>() {
|
||||
|
||||
@Override
|
||||
public List<Episode> call() throws Exception {
|
||||
List<SearchResult> results = provider.search(query, locale);
|
||||
|
||||
// select search result
|
||||
if (results.size() > 0) {
|
||||
SearchResult selectedSearchResult = selectSearchResult(query, results, selectionMemory, autodetection, parent);
|
||||
|
||||
if (selectedSearchResult != null) {
|
||||
return provider.getEpisodeList(selectedSearchResult, sortOrder, locale);
|
||||
}
|
||||
protected Set<Episode> fetchEpisodeSet(List<File> files, Collection<String> querySet, SortOrder sortOrder, Locale locale, Map<String, SearchResult> selectionMemory, boolean autodetection, Component parent) throws Exception {
|
||||
// detect series names and fetch episode lists in parallel
|
||||
List<Future<List<Episode>>> tasks = querySet.stream().map(q -> {
|
||||
return requestThreadPool.submit(() -> {
|
||||
// select search result
|
||||
List<SearchResult> results = provider.search(q, locale);
|
||||
if (results.size() > 0) {
|
||||
SearchResult selectedSearchResult = selectSearchResult(files, q, results, selectionMemory, autodetection, parent);
|
||||
if (selectedSearchResult != null) {
|
||||
return provider.getEpisodeList(selectedSearchResult, sortOrder, locale);
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return new ArrayList<Episode>();
|
||||
});
|
||||
}
|
||||
}).collect(toList());
|
||||
|
||||
// fetch episode lists concurrently and merge all episodes
|
||||
// merge all episodes
|
||||
Set<Episode> episodes = new LinkedHashSet<Episode>();
|
||||
|
||||
for (Future<List<Episode>> future : requestThreadPool.invokeAll(tasks)) {
|
||||
episodes.addAll(future.get());
|
||||
for (Future<List<Episode>> it : tasks) {
|
||||
episodes.addAll(it.get());
|
||||
}
|
||||
|
||||
// all background workers have finished
|
||||
return episodes;
|
||||
}
|
||||
|
||||
|
@ -195,101 +165,68 @@ class EpisodeListMatcher implements AutoCompleteMatcher {
|
|||
// focus on movie and subtitle files
|
||||
List<File> mediaFiles = filter(fileset, VIDEO_FILES, SUBTITLE_FILES);
|
||||
|
||||
// assume that many shows will be matched, do it folder by folder
|
||||
List<Callable<List<Match<File, ?>>>> tasks = new ArrayList<Callable<List<Match<File, ?>>>>();
|
||||
|
||||
// remember user decisions and only bother user once
|
||||
Map<String, SearchResult> selectionMemory = new TreeMap<String, SearchResult>(CommonSequenceMatcher.getLenientCollator(Locale.ENGLISH));
|
||||
Map<String, List<String>> inputMemory = new TreeMap<String, List<String>>(CommonSequenceMatcher.getLenientCollator(Locale.ENGLISH));
|
||||
|
||||
// detect series names and create episode list fetch tasks
|
||||
List<Future<List<Match<File, ?>>>> tasks = new ArrayList<Future<List<Match<File, ?>>>>();
|
||||
|
||||
if (strict) {
|
||||
// in strict mode simply process file-by-file (ignoring all files that don't contain clear SxE patterns)
|
||||
for (File file : mediaFiles) {
|
||||
if (parseEpisodeNumber(file, false) != null || parseDate(file) != null) {
|
||||
tasks.add(new Callable<List<Match<File, ?>>>() {
|
||||
|
||||
@Override
|
||||
public List<Match<File, ?>> call() throws Exception {
|
||||
return matchEpisodeSet(singletonList(file), detectSeriesNames(singleton(file), anime, locale), sortOrder, strict, locale, autodetection, selectionMemory, inputMemory, parent);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
mediaFiles.stream().filter(f -> isEpisode(f, false)).map(f -> {
|
||||
return workerThreadPool.submit(() -> {
|
||||
return matchEpisodeSet(singletonList(f), detectSeriesNames(singleton(f), anime, locale), sortOrder, strict, locale, autodetection, selectionMemory, inputMemory, parent);
|
||||
});
|
||||
}).forEach(tasks::add);
|
||||
} else {
|
||||
// in non-strict mode use the complicated (more powerful but also more error prone) match-batch-by-batch logic
|
||||
for (Entry<Set<File>, Set<String>> sameSeriesGroup : mapSeriesNamesByFiles(mediaFiles, locale, anime).entrySet()) {
|
||||
final List<List<File>> batchSets = new ArrayList<List<File>>();
|
||||
final Collection<String> queries = sameSeriesGroup.getValue();
|
||||
mapSeriesNamesByFiles(mediaFiles, locale, anime).forEach((f, n) -> {
|
||||
// 1. handle series name batch set all at once -> only 1 batch set
|
||||
// 2. files don't seem to belong to any series -> handle folder per folder -> multiple batch sets
|
||||
Collection<List<File>> batches = n != null && n.size() > 0 ? singleton(new ArrayList<File>(f)) : mapByFolder(f).values();
|
||||
|
||||
if (queries != null && queries.size() > 0) {
|
||||
// handle series name batch set all at once -> only 1 batch set
|
||||
batchSets.add(new ArrayList<File>(sameSeriesGroup.getKey()));
|
||||
} else {
|
||||
// these files don't seem to belong to any series -> handle folder per folder -> multiple batch sets
|
||||
batchSets.addAll(mapByFolder(sameSeriesGroup.getKey()).values());
|
||||
}
|
||||
|
||||
for (final List<File> batchSet : batchSets) {
|
||||
tasks.add(new Callable<List<Match<File, ?>>>() {
|
||||
|
||||
@Override
|
||||
public List<Match<File, ?>> call() throws Exception {
|
||||
return matchEpisodeSet(batchSet, queries, sortOrder, strict, locale, autodetection, selectionMemory, inputMemory, parent);
|
||||
}
|
||||
batches.stream().map(b -> {
|
||||
return workerThreadPool.submit(() -> {
|
||||
return matchEpisodeSet(b, n, sortOrder, strict, locale, autodetection, selectionMemory, inputMemory, parent);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// match folder per folder in parallel
|
||||
ExecutorService executor = Executors.newFixedThreadPool(getPreferredThreadPoolSize());
|
||||
|
||||
try {
|
||||
// merge all episodes
|
||||
List<Match<File, ?>> matches = new ArrayList<Match<File, ?>>();
|
||||
for (Future<List<Match<File, ?>>> future : executor.invokeAll(tasks)) {
|
||||
// make sure each episode has unique object data
|
||||
for (Match<File, ?> it : future.get()) {
|
||||
matches.add(new Match<File, Episode>(it.getValue(), ((Episode) it.getCandidate()).clone()));
|
||||
}
|
||||
}
|
||||
|
||||
// handle derived files
|
||||
List<Match<File, ?>> derivateMatches = new ArrayList<Match<File, ?>>();
|
||||
SortedSet<File> derivateFiles = new TreeSet<File>(fileset);
|
||||
derivateFiles.removeAll(mediaFiles);
|
||||
|
||||
for (File file : derivateFiles) {
|
||||
for (Match<File, ?> match : matches) {
|
||||
if (file.getPath().startsWith(match.getValue().getParentFile().getPath()) && isDerived(file, match.getValue()) && match.getCandidate() instanceof Episode) {
|
||||
derivateMatches.add(new Match<File, Object>(file, ((Episode) match.getCandidate()).clone()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add matches from other files that are linked via filenames
|
||||
matches.addAll(derivateMatches);
|
||||
|
||||
// restore original order
|
||||
Collections.sort(matches, new Comparator<Match<File, ?>>() {
|
||||
|
||||
@Override
|
||||
public int compare(Match<File, ?> o1, Match<File, ?> o2) {
|
||||
return fileset.indexOf(o1.getValue()) - fileset.indexOf(o2.getValue());
|
||||
}
|
||||
}).forEach(tasks::add);
|
||||
});
|
||||
|
||||
// all background workers have finished
|
||||
return matches;
|
||||
} finally {
|
||||
// destroy background threads
|
||||
executor.shutdownNow();
|
||||
}
|
||||
|
||||
// merge episode matches
|
||||
List<Match<File, ?>> matches = new ArrayList<Match<File, ?>>();
|
||||
for (Future<List<Match<File, ?>>> future : tasks) {
|
||||
// make sure each episode has unique object data
|
||||
for (Match<File, ?> it : future.get()) {
|
||||
matches.add(new Match<File, Episode>(it.getValue(), ((Episode) it.getCandidate()).clone()));
|
||||
}
|
||||
}
|
||||
|
||||
// handle derived files
|
||||
List<Match<File, ?>> derivateMatches = new ArrayList<Match<File, ?>>();
|
||||
Set<File> derivateFiles = new TreeSet<File>(fileset);
|
||||
derivateFiles.removeAll(mediaFiles);
|
||||
|
||||
for (File file : derivateFiles) {
|
||||
for (Match<File, ?> match : matches) {
|
||||
if (file.getPath().startsWith(match.getValue().getParentFile().getPath()) && isDerived(file, match.getValue()) && match.getCandidate() instanceof Episode) {
|
||||
derivateMatches.add(new Match<File, Object>(file, ((Episode) match.getCandidate()).clone()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add matches from other files that are linked via filenames
|
||||
matches.addAll(derivateMatches);
|
||||
|
||||
// restore original order
|
||||
matches.sort(comparing(Match::getValue, new OriginalOrder<File>(files)));
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
public List<Match<File, ?>> matchEpisodeSet(final List<File> files, Collection<String> queries, SortOrder sortOrder, boolean strict, Locale locale, boolean autodetection, Map<String, SearchResult> selectionMemory, Map<String, List<String>> inputMemory, Component parent) throws Exception {
|
||||
public List<Match<File, ?>> matchEpisodeSet(List<File> files, Collection<String> queries, SortOrder sortOrder, boolean strict, Locale locale, boolean autodetection, Map<String, SearchResult> selectionMemory, Map<String, List<String>> inputMemory, Component parent) throws Exception {
|
||||
Set<Episode> episodes = emptySet();
|
||||
|
||||
// detect series name and fetch episode list
|
||||
|
@ -297,7 +234,7 @@ class EpisodeListMatcher implements AutoCompleteMatcher {
|
|||
if (queries != null && queries.size() > 0) {
|
||||
// only allow one fetch session at a time so later requests can make use of cached results
|
||||
synchronized (providerLock) {
|
||||
episodes = fetchEpisodeSet(queries, sortOrder, locale, selectionMemory, autodetection, parent);
|
||||
episodes = fetchEpisodeSet(files, queries, sortOrder, locale, selectionMemory, autodetection, parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -312,7 +249,7 @@ class EpisodeListMatcher implements AutoCompleteMatcher {
|
|||
synchronized (inputMemory) {
|
||||
input = inputMemory.get(suggestion);
|
||||
if (input == null || suggestion == null || suggestion.isEmpty()) {
|
||||
input = showMultiValueInputDialog(getQueryInputMessage(files), suggestion, parentPathHint, parent);
|
||||
input = showMultiValueInputDialog(getQueryInputMessage("Enter series name:", files), suggestion, parentPathHint, parent);
|
||||
inputMemory.put(suggestion, input);
|
||||
}
|
||||
}
|
||||
|
@ -320,7 +257,7 @@ class EpisodeListMatcher implements AutoCompleteMatcher {
|
|||
if (input != null && input.size() > 0) {
|
||||
// only allow one fetch session at a time so later requests can make use of cached results
|
||||
synchronized (providerLock) {
|
||||
episodes = fetchEpisodeSet(input, sortOrder, locale, new HashMap<String, SearchResult>(), false, parent);
|
||||
episodes = fetchEpisodeSet(files, input, sortOrder, locale, new HashMap<String, SearchResult>(), false, parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -344,56 +281,53 @@ class EpisodeListMatcher implements AutoCompleteMatcher {
|
|||
return matches;
|
||||
}
|
||||
|
||||
protected String getQueryInputMessage(List<File> files) throws Exception {
|
||||
int limit = 20;
|
||||
List<File> selection = new ArrayList<File>(files);
|
||||
if (selection.size() > limit) {
|
||||
sort(selection, new Comparator<File>() {
|
||||
protected Collection<File> getFilesForQuery(Collection<File> files, String query) {
|
||||
Pattern pattern = Pattern.compile(query.isEmpty() ? ".+" : normalizePunctuation(query).replaceAll("\\W+", ".+"), Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CHARACTER_CLASS);
|
||||
List<File> selection = files.stream().filter(f -> find(f.getPath(), pattern)).collect(toList());
|
||||
return selection.size() > 0 ? selection : files;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(File o1, File o2) {
|
||||
return Long.compare(o2.length(), o1.length());
|
||||
}
|
||||
});
|
||||
selection = selection.subList(0, limit);
|
||||
}
|
||||
protected String getQueryInputMessage(String message, Collection<File> files) throws Exception {
|
||||
List<File> selection = files.stream().sorted(comparing(File::length).reversed()).limit(5).collect(toList());
|
||||
|
||||
StringBuilder html = new StringBuilder(512);
|
||||
html.append("<html>");
|
||||
html.append("Unable to identify the following files:").append("<br>");
|
||||
if (selection.size() > 0) {
|
||||
html.append("Failed to identify some of the following files:").append("<br>");
|
||||
|
||||
for (File file : sortByUniquePath(selection)) {
|
||||
html.append("<nobr>");
|
||||
html.append("• ");
|
||||
for (File file : sortByUniquePath(selection)) {
|
||||
html.append("<nobr>");
|
||||
html.append("• ");
|
||||
|
||||
File path = getStructurePathTail(file);
|
||||
if (path == null) {
|
||||
path = getRelativePathTail(file, 3);
|
||||
File path = getStructurePathTail(file);
|
||||
if (path == null) {
|
||||
path = getRelativePathTail(file, 3);
|
||||
}
|
||||
|
||||
new TextColorizer().colorizePath(html, path, true);
|
||||
html.append("</nobr>");
|
||||
html.append("<br>");
|
||||
}
|
||||
|
||||
if (selection.size() < files.size()) {
|
||||
html.append("• ").append("…").append("<br>");
|
||||
}
|
||||
|
||||
new TextColorizer().colorizePath(html, path, true);
|
||||
html.append("</nobr>");
|
||||
html.append("<br>");
|
||||
}
|
||||
|
||||
if (selection.size() < files.size()) {
|
||||
html.append("• ").append("…").append("<br>");
|
||||
}
|
||||
|
||||
html.append("<br>");
|
||||
html.append("Please enter series name:");
|
||||
html.append(message);
|
||||
html.append("</html>");
|
||||
return html.toString();
|
||||
}
|
||||
|
||||
public List<Match<File, ?>> justFetchEpisodeList(final SortOrder sortOrder, final Locale locale, final Component parent) throws Exception {
|
||||
public List<Match<File, ?>> justFetchEpisodeList(SortOrder sortOrder, Locale locale, Component parent) throws Exception {
|
||||
// require user input
|
||||
List<String> input = showMultiValueInputDialog("Enter series name:", "", "Fetch Episode List", parent);
|
||||
|
||||
List<Match<File, ?>> matches = new ArrayList<Match<File, ?>>();
|
||||
if (input.size() > 0) {
|
||||
synchronized (providerLock) {
|
||||
Set<Episode> episodes = fetchEpisodeSet(input, sortOrder, locale, new HashMap<String, SearchResult>(), false, parent);
|
||||
Set<Episode> episodes = fetchEpisodeSet(emptyList(), input, sortOrder, locale, new HashMap<String, SearchResult>(), false, parent);
|
||||
for (Episode it : episodes) {
|
||||
matches.add(new Match<File, Episode>(null, it));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
package net.filebot.ui.rename;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
class OriginalOrder<T> implements Comparator<T> {
|
||||
|
||||
private Map<T, Integer> index;
|
||||
|
||||
public OriginalOrder(Collection<T> values) {
|
||||
this.index = new HashMap<T, Integer>(values.size());
|
||||
|
||||
int i = 0;
|
||||
for (T it : values) {
|
||||
index.put(it, i++);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(T a, T b) {
|
||||
return index.get(a).compareTo(index.get(b));
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue