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.lang.String.*;
|
||||||
|
import static java.util.Collections.*;
|
||||||
import static net.sourceforge.filebot.MediaTypes.*;
|
import static net.sourceforge.filebot.MediaTypes.*;
|
||||||
import static net.sourceforge.filebot.WebServices.*;
|
import static net.sourceforge.filebot.WebServices.*;
|
||||||
import static net.sourceforge.filebot.cli.CLILogging.*;
|
import static net.sourceforge.filebot.cli.CLILogging.*;
|
||||||
import static net.sourceforge.filebot.hash.VerificationUtilities.*;
|
import static net.sourceforge.filebot.hash.VerificationUtilities.*;
|
||||||
import static net.sourceforge.filebot.subtitle.SubtitleUtilities.*;
|
import static net.sourceforge.filebot.subtitle.SubtitleUtilities.*;
|
||||||
|
import static net.sourceforge.filebot.ui.rename.MatchSimilarityMetric.*;
|
||||||
import static net.sourceforge.tuned.FileUtilities.*;
|
import static net.sourceforge.tuned.FileUtilities.*;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@ -16,6 +18,7 @@ import java.nio.ByteBuffer;
|
|||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -27,6 +30,10 @@ import java.util.TreeMap;
|
|||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
import java.util.AbstractMap.SimpleImmutableEntry;
|
import java.util.AbstractMap.SimpleImmutableEntry;
|
||||||
import java.util.Map.Entry;
|
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.Analytics;
|
||||||
import net.sourceforge.filebot.MediaTypes;
|
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.subtitle.SubtitleFormat;
|
||||||
import net.sourceforge.filebot.ui.Language;
|
import net.sourceforge.filebot.ui.Language;
|
||||||
import net.sourceforge.filebot.ui.rename.HistorySpooler;
|
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.ArchiveType;
|
||||||
import net.sourceforge.filebot.vfs.MemoryFile;
|
import net.sourceforge.filebot.vfs.MemoryFile;
|
||||||
import net.sourceforge.filebot.web.Episode;
|
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 {
|
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()));
|
CLILogger.config(format("Rename episodes using [%s]", db.getName()));
|
||||||
List<File> mediaFiles = filter(files, VIDEO_FILES, SUBTITLE_FILES);
|
List<File> mediaFiles = filter(files, VIDEO_FILES, SUBTITLE_FILES);
|
||||||
|
Collection<String> seriesNames;
|
||||||
|
|
||||||
// auto-detect series name if not given
|
// auto-detect series name if not given
|
||||||
if (query == null) {
|
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) {
|
if (seriesNames.isEmpty() || (strict && seriesNames.size() > 1)) {
|
||||||
query = possibleNames.iterator().next();
|
throw new Exception("Failed to auto-detect series name: " + seriesNames);
|
||||||
CLILogger.config("Auto-detected series name: " + possibleNames);
|
|
||||||
} else {
|
|
||||||
throw new Exception("Failed to auto-detect series name: " + possibleNames);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// similarity metrics for matching
|
||||||
SearchResult hit = selectSearchResult(query, db.search(query, locale), strict);
|
SimilarityMetric[] sequence;
|
||||||
|
if (strict) {
|
||||||
// fetch episode list
|
sequence = new SimilarityMetric[] { StrictEpisodeIdentifier, StrictName }; // use SEI for matching and SN for excluding false positives
|
||||||
List<Episode> episodes = db.getEpisodeList(hit, locale);
|
} else {
|
||||||
|
sequence = new SimilarityMetric[] { EpisodeIdentifier, Name, Numeric }; // same as in GUI
|
||||||
|
}
|
||||||
|
|
||||||
List<Match<File, Episode>> matches = new ArrayList<Match<File, Episode>>();
|
List<Match<File, Episode>> matches = new ArrayList<Match<File, Episode>>();
|
||||||
matches.addAll(match(filter(mediaFiles, VIDEO_FILES), episodes, strict));
|
matches.addAll(match(filter(mediaFiles, VIDEO_FILES), episodes, sequence));
|
||||||
matches.addAll(match(filter(mediaFiles, SUBTITLE_FILES), episodes, strict));
|
matches.addAll(match(filter(mediaFiles, SUBTITLE_FILES), episodes, sequence));
|
||||||
|
|
||||||
if (matches.isEmpty()) {
|
if (matches.isEmpty()) {
|
||||||
throw new RuntimeException("Unable to match files to episode data");
|
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 {
|
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()));
|
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 {
|
private List<Match<File, Episode>> match(Collection<File> files, Collection<Episode> episodes, SimilarityMetric[] sequence) 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 };
|
|
||||||
}
|
|
||||||
|
|
||||||
// always use strict fail-fast matcher
|
// always use strict fail-fast matcher
|
||||||
Matcher<File, Episode> matcher = new Matcher<File, Episode>(files, episodes, true, sequence);
|
Matcher<File, Episode> matcher = new Matcher<File, Episode>(files, episodes, true, sequence);
|
||||||
List<Match<File, Episode>> matches = matcher.match();
|
List<Match<File, Episode>> matches = matcher.match();
|
||||||
|
@ -121,6 +121,23 @@ public enum MatchSimilarityMetric implements SimilarityMetric {
|
|||||||
// simplify file name, if possible
|
// simplify file name, if possible
|
||||||
return super.normalize(normalizeFile(object));
|
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
|
// inner metric
|
||||||
|
Loading…
Reference in New Issue
Block a user