mirror of
https://github.com/mitb-archive/filebot
synced 2024-11-15 22:05:00 -05:00
Bugfixes, optimizations, improved user-interaction behaviour
This commit is contained in:
parent
397fb14be7
commit
612a243518
@ -89,7 +89,7 @@ public class AutoDetection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isAnime(File f) {
|
public boolean isAnime(File f) {
|
||||||
if (MediaDetection.parseEpisodeNumber(f.getName(), false).isEmpty()) {
|
if (MediaDetection.parseEpisodeNumber(f.getName(), false) == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (anyMatch(f.getParentFile(), ANIME_PATTERN) || find(f.getName(), ANIME_EPISODE_PATTERN) || find(f.getName(), EMBEDDED_CHECKSUM)) {
|
if (anyMatch(f.getParentFile(), ANIME_PATTERN) || find(f.getName(), ANIME_EPISODE_PATTERN) || find(f.getName(), EMBEDDED_CHECKSUM)) {
|
||||||
|
@ -18,7 +18,6 @@ import java.awt.Component;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@ -55,6 +54,10 @@ class EpisodeListMatcher implements AutoCompleteMatcher {
|
|||||||
private EpisodeListProvider provider;
|
private EpisodeListProvider provider;
|
||||||
private boolean anime;
|
private boolean anime;
|
||||||
|
|
||||||
|
// remember user decisions
|
||||||
|
private Map<String, SearchResult> selectionMemory = new TreeMap<String, SearchResult>(getLenientCollator(Locale.ENGLISH));
|
||||||
|
private Map<String, List<String>> inputMemory = new TreeMap<String, List<String>>(getLenientCollator(Locale.ENGLISH));
|
||||||
|
|
||||||
public EpisodeListMatcher(EpisodeListProvider provider, boolean anime) {
|
public EpisodeListMatcher(EpisodeListProvider provider, boolean anime) {
|
||||||
this.provider = provider;
|
this.provider = provider;
|
||||||
this.anime = anime;
|
this.anime = anime;
|
||||||
@ -64,7 +67,159 @@ class EpisodeListMatcher implements AutoCompleteMatcher {
|
|||||||
return Cache.getCache("selection_" + provider.getName(), CacheType.Persistent).cast(SearchResult.class);
|
return Cache.getCache("selection_" + provider.getName(), CacheType.Persistent).cast(SearchResult.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected SearchResult selectSearchResult(List<File> files, String query, List<SearchResult> options, Map<String, SearchResult> selectionMemory, boolean autodetection, Component parent) throws Exception {
|
@Override
|
||||||
|
public List<Match<File, ?>> match(Collection<File> files, boolean strict, SortOrder sortOrder, Locale locale, boolean autodetection, Component parent) throws Exception {
|
||||||
|
if (files.isEmpty()) {
|
||||||
|
return justFetchEpisodeList(sortOrder, locale, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore sample files
|
||||||
|
List<File> fileset = autodetection ? filter(files, not(getClutterFileFilter())) : new ArrayList<File>(files);
|
||||||
|
|
||||||
|
// focus on movie and subtitle files
|
||||||
|
List<File> mediaFiles = filter(fileset, VIDEO_FILES, SUBTITLE_FILES);
|
||||||
|
|
||||||
|
// merge episode matches
|
||||||
|
List<Match<File, ?>> matches = new ArrayList<Match<File, ?>>();
|
||||||
|
|
||||||
|
ExecutorService workerThreadPool = Executors.newFixedThreadPool(getPreferredThreadPoolSize());
|
||||||
|
try {
|
||||||
|
// 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)
|
||||||
|
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, parent);
|
||||||
|
});
|
||||||
|
}).forEach(tasks::add);
|
||||||
|
} else {
|
||||||
|
// in non-strict mode use the complicated (more powerful but also more error prone) match-batch-by-batch logic
|
||||||
|
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();
|
||||||
|
|
||||||
|
batches.stream().map(b -> {
|
||||||
|
return workerThreadPool.submit(() -> {
|
||||||
|
return matchEpisodeSet(b, n, sortOrder, strict, locale, autodetection, parent);
|
||||||
|
});
|
||||||
|
}).forEach(tasks::add);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
workerThreadPool.shutdownNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(List<File> files, Collection<String> queries, SortOrder sortOrder, boolean strict, Locale locale, boolean autodetection, Component parent) throws Exception {
|
||||||
|
Collection<Episode> episodes = emptySet();
|
||||||
|
|
||||||
|
// detect series name and fetch episode list
|
||||||
|
if (autodetection) {
|
||||||
|
if (queries != null && queries.size() > 0) {
|
||||||
|
// only allow one fetch session at a time so later requests can make use of cached results
|
||||||
|
episodes = fetchEpisodeSet(files, queries, sortOrder, locale, autodetection, parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// require user input if auto-detection has failed or has been disabled
|
||||||
|
if (episodes.isEmpty() && !strict) {
|
||||||
|
List<String> detectedSeriesNames = detectSeriesNames(files, anime, locale);
|
||||||
|
String suggestion = detectedSeriesNames.size() > 0 ? join(detectedSeriesNames, "; ") : normalizePunctuation(getName(files.get(0)));
|
||||||
|
|
||||||
|
synchronized (inputMemory) {
|
||||||
|
List<String> input = inputMemory.get(suggestion);
|
||||||
|
if (input == null || suggestion == null || suggestion.isEmpty()) {
|
||||||
|
synchronized (parent) {
|
||||||
|
input = showMultiValueInputDialog(getQueryInputMessage("Please identify the following files:", "Enter series name:", files), suggestion, provider.getName(), parent);
|
||||||
|
}
|
||||||
|
inputMemory.put(suggestion, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input != null && input.size() > 0) {
|
||||||
|
// only allow one fetch session at a time so later requests can make use of cached results
|
||||||
|
episodes = fetchEpisodeSet(files, input, sortOrder, locale, false, parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// find file/episode matches
|
||||||
|
List<Match<File, ?>> matches = new ArrayList<Match<File, ?>>();
|
||||||
|
|
||||||
|
// group by subtitles first and then by files in general
|
||||||
|
if (episodes.size() > 0) {
|
||||||
|
for (List<File> filesPerType : mapByMediaExtension(files).values()) {
|
||||||
|
EpisodeMatcher matcher = new EpisodeMatcher(filesPerType, episodes, strict);
|
||||||
|
for (Match<File, Object> it : matcher.match()) {
|
||||||
|
// in strict mode sanity check the result and only pass back good matches
|
||||||
|
if (!strict || isEpisodeNumberMatch(it.getValue(), (Episode) it.getCandidate())) {
|
||||||
|
matches.add(new Match<File, Episode>(it.getValue(), ((Episode) it.getCandidate()).clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Set<Episode> fetchEpisodeSet(List<File> files, Collection<String> querySet, SortOrder sortOrder, Locale locale, boolean autodetection, Component parent) throws Exception {
|
||||||
|
// only allow one fetch session at a time so later requests can make use of cached results
|
||||||
|
// 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> options = provider.search(q, locale);
|
||||||
|
|
||||||
|
if (options.size() > 0) {
|
||||||
|
SearchResult selectedSearchResult = selectSearchResult(files, q, options, autodetection, parent);
|
||||||
|
if (selectedSearchResult != null) {
|
||||||
|
return provider.getEpisodeList(selectedSearchResult, sortOrder, locale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (List<Episode>) EMPTY_LIST;
|
||||||
|
});
|
||||||
|
}).collect(toList());
|
||||||
|
|
||||||
|
// merge all episodes
|
||||||
|
Set<Episode> episodes = new LinkedHashSet<Episode>();
|
||||||
|
for (Future<List<Episode>> it : tasks) {
|
||||||
|
episodes.addAll(it.get());
|
||||||
|
}
|
||||||
|
return episodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected SearchResult selectSearchResult(List<File> files, String query, List<SearchResult> options, boolean autodetection, Component parent) throws Exception {
|
||||||
if (options.size() == 1) {
|
if (options.size() == 1) {
|
||||||
return options.get(0);
|
return options.get(0);
|
||||||
}
|
}
|
||||||
@ -85,7 +240,7 @@ class EpisodeListMatcher implements AutoCompleteMatcher {
|
|||||||
// multiple results have been found, user must select one
|
// multiple results have been found, user must select one
|
||||||
SelectDialog<SearchResult> selectDialog = new SelectDialog<SearchResult>(parent, options, true, false, header);
|
SelectDialog<SearchResult> selectDialog = new SelectDialog<SearchResult>(parent, options, true, false, header);
|
||||||
selectDialog.setTitle(provider.getName());
|
selectDialog.setTitle(provider.getName());
|
||||||
selectDialog.getMessageLabel().setText("<html>Select best match for \"<b>" + query + "</b>\":</html>");
|
selectDialog.getMessageLabel().setText("<html>Select best match for \"<b>" + escapeHTML(query) + "</b>\":</html>");
|
||||||
selectDialog.getCancelAction().putValue(Action.NAME, "Skip");
|
selectDialog.getCancelAction().putValue(Action.NAME, "Skip");
|
||||||
selectDialog.pack();
|
selectDialog.pack();
|
||||||
|
|
||||||
@ -135,162 +290,6 @@ class EpisodeListMatcher implements AutoCompleteMatcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Set<Episode> fetchEpisodeSet(List<File> files, Collection<String> querySet, SortOrder sortOrder, Locale locale, Map<String, SearchResult> selectionMemory, boolean autodetection, Component parent) throws Exception {
|
|
||||||
// only allow one fetch session at a time so later requests can make use of cached results
|
|
||||||
synchronized (this) {
|
|
||||||
// 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> options = provider.search(q, locale);
|
|
||||||
|
|
||||||
if (options.size() > 0) {
|
|
||||||
SearchResult selectedSearchResult = selectSearchResult(files, q, options, selectionMemory, autodetection, parent);
|
|
||||||
if (selectedSearchResult != null) {
|
|
||||||
return provider.getEpisodeList(selectedSearchResult, sortOrder, locale);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (List<Episode>) EMPTY_LIST;
|
|
||||||
});
|
|
||||||
}).collect(toList());
|
|
||||||
|
|
||||||
// merge all episodes
|
|
||||||
Set<Episode> episodes = new LinkedHashSet<Episode>();
|
|
||||||
for (Future<List<Episode>> it : tasks) {
|
|
||||||
episodes.addAll(it.get());
|
|
||||||
}
|
|
||||||
return episodes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Match<File, ?>> match(Collection<File> files, boolean strict, SortOrder sortOrder, Locale locale, boolean autodetection, Component parent) throws Exception {
|
|
||||||
if (files.isEmpty()) {
|
|
||||||
return justFetchEpisodeList(sortOrder, locale, parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ignore sample files
|
|
||||||
List<File> fileset = autodetection ? filter(files, not(getClutterFileFilter())) : new ArrayList<File>(files);
|
|
||||||
|
|
||||||
// focus on movie and subtitle files
|
|
||||||
List<File> mediaFiles = filter(fileset, VIDEO_FILES, SUBTITLE_FILES);
|
|
||||||
|
|
||||||
// remember user decisions and only bother user once
|
|
||||||
Map<String, SearchResult> selectionMemory = new TreeMap<String, SearchResult>(getLenientCollator(Locale.ENGLISH));
|
|
||||||
Map<String, List<String>> inputMemory = new TreeMap<String, List<String>>(getLenientCollator(Locale.ENGLISH));
|
|
||||||
|
|
||||||
// merge episode matches
|
|
||||||
List<Match<File, ?>> matches = new ArrayList<Match<File, ?>>();
|
|
||||||
|
|
||||||
ExecutorService workerThreadPool = Executors.newFixedThreadPool(getPreferredThreadPoolSize());
|
|
||||||
try {
|
|
||||||
// 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)
|
|
||||||
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
|
|
||||||
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();
|
|
||||||
|
|
||||||
batches.stream().map(b -> {
|
|
||||||
return workerThreadPool.submit(() -> {
|
|
||||||
return matchEpisodeSet(b, n, sortOrder, strict, locale, autodetection, selectionMemory, inputMemory, parent);
|
|
||||||
});
|
|
||||||
}).forEach(tasks::add);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
workerThreadPool.shutdownNow();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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(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 {
|
|
||||||
Collection<Episode> episodes = emptySet();
|
|
||||||
|
|
||||||
// detect series name and fetch episode list
|
|
||||||
if (autodetection) {
|
|
||||||
if (queries != null && queries.size() > 0) {
|
|
||||||
// only allow one fetch session at a time so later requests can make use of cached results
|
|
||||||
episodes = fetchEpisodeSet(files, queries, sortOrder, locale, selectionMemory, autodetection, parent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// require user input if auto-detection has failed or has been disabled
|
|
||||||
if (episodes.isEmpty() && !strict) {
|
|
||||||
List<String> detectedSeriesNames = detectSeriesNames(files, anime, locale);
|
|
||||||
String suggestion = detectedSeriesNames.size() > 0 ? join(detectedSeriesNames, "; ") : normalizePunctuation(getName(files.get(0)));
|
|
||||||
|
|
||||||
synchronized (inputMemory) {
|
|
||||||
List<String> input = inputMemory.get(suggestion);
|
|
||||||
if (input == null || suggestion == null || suggestion.isEmpty()) {
|
|
||||||
input = showMultiValueInputDialog(getQueryInputMessage("Please identify the following files:", "Enter series name:", files), suggestion, provider.getName(), parent);
|
|
||||||
inputMemory.put(suggestion, input);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input != null && input.size() > 0) {
|
|
||||||
// only allow one fetch session at a time so later requests can make use of cached results
|
|
||||||
episodes = fetchEpisodeSet(files, input, sortOrder, locale, new HashMap<String, SearchResult>(), false, parent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// find file/episode matches
|
|
||||||
List<Match<File, ?>> matches = new ArrayList<Match<File, ?>>();
|
|
||||||
|
|
||||||
// group by subtitles first and then by files in general
|
|
||||||
if (episodes.size() > 0) {
|
|
||||||
for (List<File> filesPerType : mapByMediaExtension(files).values()) {
|
|
||||||
EpisodeMatcher matcher = new EpisodeMatcher(filesPerType, episodes, strict);
|
|
||||||
for (Match<File, Object> it : matcher.match()) {
|
|
||||||
// in strict mode sanity check the result and only pass back good matches
|
|
||||||
if (!strict || isEpisodeNumberMatch(it.getValue(), (Episode) it.getCandidate())) {
|
|
||||||
matches.add(new Match<File, Episode>(it.getValue(), ((Episode) it.getCandidate()).clone()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return matches;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Collection<File> getFilesForQuery(Collection<File> files, String query) {
|
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);
|
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());
|
List<File> selection = files.stream().filter(f -> find(f.getPath(), pattern)).collect(toList());
|
||||||
@ -304,7 +303,7 @@ class EpisodeListMatcher implements AutoCompleteMatcher {
|
|||||||
html.append("<html>");
|
html.append("<html>");
|
||||||
if (selection.size() > 0) {
|
if (selection.size() > 0) {
|
||||||
if (header != null) {
|
if (header != null) {
|
||||||
html.append(header).append("<br>");
|
html.append(escapeHTML(header)).append("<br>");
|
||||||
}
|
}
|
||||||
for (File file : sortByUniquePath(selection)) {
|
for (File file : sortByUniquePath(selection)) {
|
||||||
html.append("<nobr>");
|
html.append("<nobr>");
|
||||||
@ -325,7 +324,7 @@ class EpisodeListMatcher implements AutoCompleteMatcher {
|
|||||||
html.append("<br>");
|
html.append("<br>");
|
||||||
}
|
}
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
html.append(message);
|
html.append(escapeHTML(message));
|
||||||
}
|
}
|
||||||
html.append("</html>");
|
html.append("</html>");
|
||||||
return html.toString();
|
return html.toString();
|
||||||
@ -337,7 +336,7 @@ class EpisodeListMatcher implements AutoCompleteMatcher {
|
|||||||
|
|
||||||
List<Match<File, ?>> matches = new ArrayList<Match<File, ?>>();
|
List<Match<File, ?>> matches = new ArrayList<Match<File, ?>>();
|
||||||
if (input.size() > 0) {
|
if (input.size() > 0) {
|
||||||
Collection<Episode> episodes = fetchEpisodeSet(emptyList(), input, sortOrder, locale, new HashMap<String, SearchResult>(), false, parent);
|
Collection<Episode> episodes = fetchEpisodeSet(emptyList(), input, sortOrder, locale, false, parent);
|
||||||
for (Episode it : episodes) {
|
for (Episode it : episodes) {
|
||||||
matches.add(new Match<File, Episode>(null, it));
|
matches.add(new Match<File, Episode>(null, it));
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,12 @@ import net.filebot.web.SortOrder;
|
|||||||
|
|
||||||
class MovieMatcher implements AutoCompleteMatcher {
|
class MovieMatcher implements AutoCompleteMatcher {
|
||||||
|
|
||||||
private final MovieIdentificationService service;
|
private MovieIdentificationService service;
|
||||||
|
|
||||||
|
// remember user decisions and only bother user once
|
||||||
|
private Set<AutoSelection> autoSelectionMode = EnumSet.noneOf(AutoSelection.class);
|
||||||
|
private Map<String, Movie> selectionMemory = new TreeMap<String, Movie>(getLenientCollator(Locale.ENGLISH));
|
||||||
|
private Map<String, String> inputMemory = new TreeMap<String, String>(getLenientCollator(Locale.ENGLISH));
|
||||||
|
|
||||||
public MovieMatcher(MovieIdentificationService service) {
|
public MovieMatcher(MovieIdentificationService service) {
|
||||||
this.service = service;
|
this.service = service;
|
||||||
@ -170,15 +175,10 @@ class MovieMatcher implements AutoCompleteMatcher {
|
|||||||
});
|
});
|
||||||
}).collect(toList());
|
}).collect(toList());
|
||||||
|
|
||||||
// remember user decisions and only bother user once
|
|
||||||
Set<AutoSelection> selectionMode = EnumSet.noneOf(AutoSelection.class);
|
|
||||||
Map<String, Movie> selectionMemory = new TreeMap<String, Movie>(getLenientCollator(locale));
|
|
||||||
Map<String, String> inputMemory = new TreeMap<String, String>(getLenientCollator(locale));
|
|
||||||
|
|
||||||
for (Future<Map<File, List<Movie>>> future : tasks) {
|
for (Future<Map<File, List<Movie>>> future : tasks) {
|
||||||
for (Entry<File, List<Movie>> it : future.get().entrySet()) {
|
for (Entry<File, List<Movie>> it : future.get().entrySet()) {
|
||||||
// auto-select movie or ask user
|
// auto-select movie or ask user
|
||||||
Movie movie = grabMovieName(it.getKey(), it.getValue(), strict, locale, autodetect, selectionMode, selectionMemory, inputMemory, parent);
|
Movie movie = grabMovieName(it.getKey(), it.getValue(), strict, locale, autodetect, parent);
|
||||||
|
|
||||||
// make sure to use language-specific movie object
|
// make sure to use language-specific movie object
|
||||||
if (movie != null) {
|
if (movie != null) {
|
||||||
@ -222,7 +222,7 @@ class MovieMatcher implements AutoCompleteMatcher {
|
|||||||
return matches;
|
return matches;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Movie grabMovieName(File movieFile, Collection<Movie> options, boolean strict, Locale locale, boolean autodetect, Set<AutoSelection> autoSelectionMode, Map<String, Movie> selectionMemory, Map<String, String> inputMemory, Component parent) throws Exception {
|
protected Movie grabMovieName(File movieFile, Collection<Movie> options, boolean strict, Locale locale, boolean autodetect, Component parent) throws Exception {
|
||||||
// allow manual user input
|
// allow manual user input
|
||||||
synchronized (selectionMemory) {
|
synchronized (selectionMemory) {
|
||||||
if (!strict && (!autodetect || options.isEmpty()) && !(autodetect && autoSelectionMode.size() > 0)) {
|
if (!strict && (!autodetect || options.isEmpty()) && !(autodetect && autoSelectionMode.size() > 0)) {
|
||||||
@ -231,27 +231,29 @@ class MovieMatcher implements AutoCompleteMatcher {
|
|||||||
|
|
||||||
if (input == null || suggestion == null || suggestion.isEmpty()) {
|
if (input == null || suggestion == null || suggestion.isEmpty()) {
|
||||||
File movieFolder = guessMovieFolder(movieFile);
|
File movieFolder = guessMovieFolder(movieFile);
|
||||||
|
synchronized (parent) {
|
||||||
input = showInputDialog(getQueryInputMessage("Please identify the following files:", "Enter movie name:", movieFile), suggestion != null && suggestion.length() > 0 ? suggestion : getName(movieFile), movieFolder == null ? movieFile.getName() : String.join(" / ", movieFolder.getName(), movieFile.getName()), parent);
|
input = showInputDialog(getQueryInputMessage("Please identify the following files:", "Enter movie name:", movieFile), suggestion != null && suggestion.length() > 0 ? suggestion : getName(movieFile), movieFolder == null ? movieFile.getName() : String.join(" / ", movieFolder.getName(), movieFile.getName()), parent);
|
||||||
|
}
|
||||||
inputMemory.put(suggestion, input);
|
inputMemory.put(suggestion, input);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input != null && input.length() > 0) {
|
if (input != null && input.length() > 0) {
|
||||||
options = service.searchMovie(input, locale);
|
options = service.searchMovie(input, locale);
|
||||||
if (options.size() > 0) {
|
if (options.size() > 0) {
|
||||||
return selectMovie(movieFile, strict, input, options, autoSelectionMode, selectionMemory, parent);
|
return selectMovie(movieFile, strict, input, options, parent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return options.isEmpty() ? null : selectMovie(movieFile, strict, null, options, autoSelectionMode, selectionMemory, parent);
|
return options.isEmpty() ? null : selectMovie(movieFile, strict, null, options, parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String getQueryInputMessage(String header, String message, File file) throws Exception {
|
protected String getQueryInputMessage(String header, String message, File file) throws Exception {
|
||||||
StringBuilder html = new StringBuilder(512);
|
StringBuilder html = new StringBuilder(512);
|
||||||
html.append("<html>");
|
html.append("<html>");
|
||||||
if (header != null) {
|
if (header != null) {
|
||||||
html.append(header).append("<br>");
|
html.append(escapeHTML(header)).append("<br>");
|
||||||
}
|
}
|
||||||
|
|
||||||
html.append("<nobr>");
|
html.append("<nobr>");
|
||||||
@ -268,7 +270,7 @@ class MovieMatcher implements AutoCompleteMatcher {
|
|||||||
|
|
||||||
html.append("<br>");
|
html.append("<br>");
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
html.append(message);
|
html.append(escapeHTML(message));
|
||||||
}
|
}
|
||||||
html.append("</html>");
|
html.append("</html>");
|
||||||
return html.toString();
|
return html.toString();
|
||||||
@ -288,7 +290,7 @@ class MovieMatcher implements AutoCompleteMatcher {
|
|||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Movie selectMovie(File movieFile, boolean strict, String userQuery, Collection<Movie> options, Set<AutoSelection> autoSelectionMode, Map<String, Movie> selectionMemory, Component parent) throws Exception {
|
protected Movie selectMovie(File movieFile, boolean strict, String userQuery, Collection<Movie> options, Component parent) throws Exception {
|
||||||
// just auto-pick singleton results
|
// just auto-pick singleton results
|
||||||
if (options.size() == 1) {
|
if (options.size() == 1) {
|
||||||
return options.iterator().next();
|
return options.iterator().next();
|
||||||
@ -356,7 +358,7 @@ class MovieMatcher implements AutoCompleteMatcher {
|
|||||||
SelectDialog<Movie> selectDialog = new SelectDialog<Movie>(parent, options, true, false, header);
|
SelectDialog<Movie> selectDialog = new SelectDialog<Movie>(parent, options, true, false, header);
|
||||||
|
|
||||||
selectDialog.setTitle(service.getName());
|
selectDialog.setTitle(service.getName());
|
||||||
selectDialog.getMessageLabel().setText("<html>Select best match for \"<b>" + query + "</b>\":</html>");
|
selectDialog.getMessageLabel().setText("<html>Select best match for \"<b>" + escapeHTML(query) + "</b>\":</html>");
|
||||||
selectDialog.getCancelAction().putValue(Action.NAME, "Skip");
|
selectDialog.getCancelAction().putValue(Action.NAME, "Skip");
|
||||||
selectDialog.pack();
|
selectDialog.pack();
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user