mirror of
https://github.com/mitb-archive/filebot
synced 2024-11-16 14:25:02 -05:00
* cli rename: support matching multiple shows to files in the same folder
This commit is contained in:
parent
3c179572c3
commit
8418389e77
@ -3,11 +3,13 @@ package net.sourceforge.filebot.cli;
|
||||
|
||||
|
||||
import static java.lang.String.*;
|
||||
import static java.util.Collections.*;
|
||||
import static net.sourceforge.filebot.MediaTypes.*;
|
||||
import static net.sourceforge.filebot.WebServices.*;
|
||||
import static net.sourceforge.filebot.cli.CLILogging.*;
|
||||
import static net.sourceforge.filebot.hash.VerificationUtilities.*;
|
||||
import static net.sourceforge.filebot.subtitle.SubtitleUtilities.*;
|
||||
import static net.sourceforge.filebot.ui.rename.MatchSimilarityMetric.*;
|
||||
import static net.sourceforge.tuned.FileUtilities.*;
|
||||
|
||||
import java.io.File;
|
||||
@ -16,6 +18,7 @@ import java.nio.ByteBuffer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
@ -27,6 +30,10 @@ import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
import java.util.AbstractMap.SimpleImmutableEntry;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import net.sourceforge.filebot.Analytics;
|
||||
import net.sourceforge.filebot.MediaTypes;
|
||||
@ -44,7 +51,6 @@ import net.sourceforge.filebot.similarity.SimilarityMetric;
|
||||
import net.sourceforge.filebot.subtitle.SubtitleFormat;
|
||||
import net.sourceforge.filebot.ui.Language;
|
||||
import net.sourceforge.filebot.ui.rename.HistorySpooler;
|
||||
import net.sourceforge.filebot.ui.rename.MatchSimilarityMetric;
|
||||
import net.sourceforge.filebot.vfs.ArchiveType;
|
||||
import net.sourceforge.filebot.vfs.MemoryFile;
|
||||
import net.sourceforge.filebot.web.Episode;
|
||||
@ -149,30 +155,35 @@ public class ArgumentProcessor {
|
||||
public Set<File> renameSeries(Collection<File> files, String query, ExpressionFormat format, EpisodeListProvider db, Locale locale, boolean strict) throws Exception {
|
||||
CLILogger.config(format("Rename episodes using [%s]", db.getName()));
|
||||
List<File> mediaFiles = filter(files, VIDEO_FILES, SUBTITLE_FILES);
|
||||
Collection<String> seriesNames;
|
||||
|
||||
// auto-detect series name if not given
|
||||
if (query == null) {
|
||||
Collection<String> possibleNames = new SeriesNameMatcher().matchAll(mediaFiles.toArray(new File[0]));
|
||||
seriesNames = new SeriesNameMatcher().matchAll(mediaFiles.toArray(new File[0]));
|
||||
|
||||
if (possibleNames.size() == 1) {
|
||||
query = possibleNames.iterator().next();
|
||||
CLILogger.config("Auto-detected series name: " + possibleNames);
|
||||
} else {
|
||||
throw new Exception("Failed to auto-detect series name: " + possibleNames);
|
||||
if (seriesNames.isEmpty() || (strict && seriesNames.size() > 1)) {
|
||||
throw new Exception("Failed to auto-detect series name: " + seriesNames);
|
||||
}
|
||||
|
||||
query = seriesNames.iterator().next();
|
||||
CLILogger.config("Auto-detected series name: " + seriesNames);
|
||||
} else {
|
||||
seriesNames = singleton(query);
|
||||
}
|
||||
|
||||
CLILogger.fine(format("Fetching episode data for [%s]", query));
|
||||
Set<Episode> episodes = fetchEpisodeSet(db, seriesNames, locale, strict);
|
||||
|
||||
// find series on the web
|
||||
SearchResult hit = selectSearchResult(query, db.search(query, locale), strict);
|
||||
|
||||
// fetch episode list
|
||||
List<Episode> episodes = db.getEpisodeList(hit, locale);
|
||||
// similarity metrics for matching
|
||||
SimilarityMetric[] sequence;
|
||||
if (strict) {
|
||||
sequence = new SimilarityMetric[] { StrictEpisodeIdentifier, StrictName }; // use SEI for matching and SN for excluding false positives
|
||||
} else {
|
||||
sequence = new SimilarityMetric[] { EpisodeIdentifier, Name, Numeric }; // same as in GUI
|
||||
}
|
||||
|
||||
List<Match<File, Episode>> matches = new ArrayList<Match<File, Episode>>();
|
||||
matches.addAll(match(filter(mediaFiles, VIDEO_FILES), episodes, strict));
|
||||
matches.addAll(match(filter(mediaFiles, SUBTITLE_FILES), episodes, strict));
|
||||
matches.addAll(match(filter(mediaFiles, VIDEO_FILES), episodes, sequence));
|
||||
matches.addAll(match(filter(mediaFiles, SUBTITLE_FILES), episodes, sequence));
|
||||
|
||||
if (matches.isEmpty()) {
|
||||
throw new RuntimeException("Unable to match files to episode data");
|
||||
@ -201,6 +212,57 @@ public class ArgumentProcessor {
|
||||
}
|
||||
|
||||
|
||||
private Set<Episode> fetchEpisodeSet(final EpisodeListProvider db, final Collection<String> names, final Locale locale, final boolean strict) 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 : names) {
|
||||
tasks.add(new Callable<List<Episode>>() {
|
||||
|
||||
@Override
|
||||
public List<Episode> call() throws Exception {
|
||||
List<SearchResult> results = db.search(query, locale);
|
||||
|
||||
// select search result
|
||||
if (results.size() > 0) {
|
||||
SearchResult selectedSearchResult = selectSearchResult(query, results, strict);
|
||||
|
||||
if (selectedSearchResult != null) {
|
||||
CLILogger.fine(format("Fetching episode data for [%s]", selectedSearchResult.getName()));
|
||||
Analytics.trackEvent(db.getName(), "FetchEpisodeList", selectedSearchResult.getName());
|
||||
return db.getEpisodeList(selectedSearchResult, locale);
|
||||
}
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// fetch episode lists concurrently
|
||||
ExecutorService executor = Executors.newCachedThreadPool();
|
||||
|
||||
try {
|
||||
// merge all episodes
|
||||
Set<Episode> episodes = new LinkedHashSet<Episode>();
|
||||
|
||||
for (Future<List<Episode>> future : executor.invokeAll(tasks)) {
|
||||
try {
|
||||
episodes.addAll(future.get());
|
||||
} catch (Exception e) {
|
||||
CLILogger.finest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// all background workers have finished
|
||||
return episodes;
|
||||
} finally {
|
||||
// destroy background threads
|
||||
executor.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Set<File> renameMovie(Collection<File> mediaFiles, String query, ExpressionFormat format, MovieIdentificationService db, Locale locale, boolean strict) throws Exception {
|
||||
CLILogger.config(format("Rename movies using [%s]", db.getName()));
|
||||
|
||||
@ -441,23 +503,7 @@ public class ArgumentProcessor {
|
||||
}
|
||||
|
||||
|
||||
private List<Match<File, Episode>> match(List<File> files, List<Episode> episodes, boolean strict) throws Exception {
|
||||
SimilarityMetric[] sequence = MatchSimilarityMetric.defaultSequence();
|
||||
|
||||
if (strict) {
|
||||
// strict SxE metric, don't allow in-between values
|
||||
SimilarityMetric strictEpisodeMetric = new SimilarityMetric() {
|
||||
|
||||
@Override
|
||||
public float getSimilarity(Object o1, Object o2) {
|
||||
return MatchSimilarityMetric.EpisodeIdentifier.getSimilarity(o1, o2) >= 1 ? 1 : 0;
|
||||
}
|
||||
};
|
||||
|
||||
// use only strict SxE metric
|
||||
sequence = new SimilarityMetric[] { strictEpisodeMetric };
|
||||
}
|
||||
|
||||
private List<Match<File, Episode>> match(Collection<File> files, Collection<Episode> episodes, SimilarityMetric[] sequence) throws Exception {
|
||||
// always use strict fail-fast matcher
|
||||
Matcher<File, Episode> matcher = new Matcher<File, Episode>(files, episodes, true, sequence);
|
||||
List<Match<File, Episode>> matches = matcher.match();
|
||||
|
@ -121,6 +121,23 @@ public enum MatchSimilarityMetric implements SimilarityMetric {
|
||||
// simplify file name, if possible
|
||||
return super.normalize(normalizeFile(object));
|
||||
}
|
||||
}),
|
||||
|
||||
StrictEpisodeIdentifier(new SimilarityMetric() {
|
||||
|
||||
@Override
|
||||
public float getSimilarity(Object o1, Object o2) {
|
||||
// strict SxE metric, don't allow in-between values
|
||||
return EpisodeIdentifier.getSimilarity(o1, o2) >= 1 ? 1 : 0;
|
||||
}
|
||||
}),
|
||||
|
||||
StrictName(new SimilarityMetric() {
|
||||
|
||||
@Override
|
||||
public float getSimilarity(Object o1, Object o2) {
|
||||
return (float) (Math.floor(Name.getSimilarity(o1, o2) * 2) / 2);
|
||||
}
|
||||
});
|
||||
|
||||
// inner metric
|
||||
|
Loading…
Reference in New Issue
Block a user