Refactor CmdlineInterface with proper types for all parameters

This commit is contained in:
Reinhard Pointner 2016-11-28 06:10:42 +08:00
parent b42149e927
commit 7a0a36b528
11 changed files with 257 additions and 229 deletions

View File

@ -15,6 +15,7 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.stream.Stream;
import net.filebot.media.XattrMetaInfoProvider;
import net.filebot.similarity.MetricAvg;
@ -40,7 +41,6 @@ import net.filebot.web.TVMazeClient;
import net.filebot.web.TheTVDBClient;
import net.filebot.web.TheTVDBClientV1;
import net.filebot.web.VideoHashSubtitleService;
import one.util.streamex.StreamEx;
/**
* Reuse the same web service client so login, cache, etc. can be shared.
@ -72,29 +72,39 @@ public final class WebServices {
public static final XattrMetaInfoProvider XattrMetaData = new XattrMetaInfoProvider();
public static final ID3Lookup MediaInfoID3 = new ID3Lookup();
public static EpisodeListProvider[] getEpisodeListProviders() {
return new EpisodeListProvider[] { TheTVDB, AniDB, TheMovieDB_TV, TVmaze };
public static Datasource[] getServices() {
return new Datasource[] { TheMovieDB, OMDb, TheTVDB, AniDB, TheMovieDB_TV, TVmaze, AcoustID, MediaInfoID3, XattrMetaData, OpenSubtitles, Shooter, TheTVDBv2, FanartTV };
}
public static MovieIdentificationService[] getMovieIdentificationServices() {
return new MovieIdentificationService[] { TheMovieDB, OMDb };
}
public static SubtitleProvider[] getSubtitleProviders() {
return new SubtitleProvider[] { OpenSubtitles };
}
public static VideoHashSubtitleService[] getVideoHashSubtitleServices(Locale locale) {
if (locale.equals(Locale.CHINESE))
return new VideoHashSubtitleService[] { OpenSubtitles, Shooter };
else
return new VideoHashSubtitleService[] { OpenSubtitles };
public static EpisodeListProvider[] getEpisodeListProviders() {
return new EpisodeListProvider[] { TheTVDB, AniDB, TheMovieDB_TV, TVmaze };
}
public static MusicIdentificationService[] getMusicIdentificationServices() {
return new MusicIdentificationService[] { AcoustID, MediaInfoID3 };
}
public static SubtitleProvider[] getSubtitleProviders(Locale locale) {
return new SubtitleProvider[] { OpenSubtitles };
}
public static VideoHashSubtitleService[] getVideoHashSubtitleServices(Locale locale) {
// special support for 射手网 for Chinese language subtitles
if (locale.equals(Locale.CHINESE)) {
return new VideoHashSubtitleService[] { OpenSubtitles, Shooter };
}
return new VideoHashSubtitleService[] { OpenSubtitles };
}
public static Datasource getService(String name) {
return getService(name, getServices());
}
public static EpisodeListProvider getEpisodeListProvider(String name) {
return getService(name, getEpisodeListProviders());
}
@ -107,8 +117,10 @@ public final class WebServices {
return getService(name, getMusicIdentificationServices());
}
public static <T extends Datasource> T getService(String name, T[] services) {
return StreamEx.of(services).findFirst(it -> it.getIdentifier().equalsIgnoreCase(name) || it.getName().equalsIgnoreCase(name)).orElse(null);
public static <T extends Datasource> T getService(String name, T... services) {
return stream(services).filter(it -> {
return it.getIdentifier().equalsIgnoreCase(name) || it.getName().equalsIgnoreCase(name);
}).findFirst().orElse(null);
}
public static final ExecutorService requestThreadPool = Executors.newCachedThreadPool();
@ -127,7 +139,8 @@ public final class WebServices {
private SearchResult merge(SearchResult prime, List<SearchResult> group) {
int id = prime.getId();
String name = prime.getName();
String[] aliasNames = StreamEx.of(group).flatMap(it -> stream(it.getAliasNames())).remove(name::equals).distinct().toArray(String[]::new);
String[] aliasNames = group.stream().flatMap(it -> stream(it.getAliasNames())).filter(n -> !n.equals(name)).distinct().toArray(String[]::new);
return new SearchResult(id, name, aliasNames);
}
@ -138,7 +151,7 @@ public final class WebServices {
Future<List<SearchResult>> localSearch = requestThreadPool.submit(() -> localIndex.get().search(query));
// combine alias names into a single search results, and keep API search name as primary name
Map<Integer, SearchResult> results = StreamEx.of(apiSearch.get()).append(localSearch.get()).groupingBy(SearchResult::getId, collectingAndThen(toList(), group -> merge(group.get(0), group)));
Map<Integer, SearchResult> results = Stream.concat(apiSearch.get().stream(), localSearch.get().stream()).collect(groupingBy(SearchResult::getId, collectingAndThen(toList(), group -> merge(group.get(0), group))));
return sortBySimilarity(results.values(), singleton(query), getSeriesMatchMetric());
}

View File

@ -2,15 +2,20 @@ package net.filebot.cli;
import static java.util.Collections.*;
import static net.filebot.Logging.*;
import static net.filebot.hash.VerificationUtilities.*;
import static net.filebot.subtitle.SubtitleUtilities.*;
import static net.filebot.util.FileUtilities.*;
import java.io.File;
import java.io.FileFilter;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.logging.Level;
import org.kohsuke.args4j.Argument;
@ -22,6 +27,14 @@ import org.kohsuke.args4j.spi.ExplicitBooleanOptionHandler;
import net.filebot.Language;
import net.filebot.StandardRenameAction;
import net.filebot.WebServices;
import net.filebot.format.ExpressionFileFilter;
import net.filebot.format.ExpressionFilter;
import net.filebot.format.ExpressionFormat;
import net.filebot.hash.HashType;
import net.filebot.subtitle.SubtitleFormat;
import net.filebot.subtitle.SubtitleNaming;
import net.filebot.web.Datasource;
import net.filebot.web.SortOrder;
public class ArgumentBean {
@ -194,12 +207,61 @@ public class ArgumentBean {
return SortOrder.forName(order);
}
public Locale getLocale() {
return new Locale(lang);
public ExpressionFormat getExpressionFormat() throws Exception {
return format == null ? null : new ExpressionFormat(format);
}
public ExpressionFilter getExpressionFilter() throws Exception {
return filter == null ? null : new ExpressionFilter(filter);
}
public FileFilter getExpressionFileFilter() throws Exception {
return filter == null ? null : new ExpressionFileFilter(filter);
}
public Datasource getDatasource() {
return db == null ? null : WebServices.getService(db);
}
public String getSearchQuery() {
return query == null || query.isEmpty() ? null : query;
}
public File getOutputPath() {
return output == null ? null : new File(output);
}
public File getAbsoluteOutputFolder() throws Exception {
return output == null ? null : new File(output).getCanonicalFile();
}
public SubtitleFormat getSubtitleOutputFormat() {
return output == null ? null : getSubtitleFormatByName(output);
}
public SubtitleNaming getSubtitleNamingFormat() {
return optional(format).map(SubtitleNaming::forName).orElse(SubtitleNaming.MATCH_VIDEO_ADD_LANGUAGE_TAG);
}
public HashType getOutputHashType() {
// support --output checksum.sfv
return optional(output).map(File::new).map(f -> getHashType(f)).orElseGet(() -> {
// support --format SFV
return optional(format).map(k -> getHashTypeByExtension(k)).orElse(HashType.SFV);
});
}
public Charset getEncoding() {
return encoding == null ? null : Charset.forName(encoding);
}
public Language getLanguage() {
return Language.findLanguage(lang);
// find language code for any input (en, eng, English, etc)
return optional(lang).map(Language::findLanguage).orElseThrow(error("Illegal language code", lang));
}
public boolean isStrict() {
return !nonStrict;
}
public Level getLogLevel() {
@ -226,4 +288,12 @@ public class ArgumentBean {
return buffer.toString();
}
private static <T> Optional<T> optional(T value) {
return Optional.ofNullable(value);
}
private static Supplier<CmdlineException> error(String message, Object value) {
return () -> new CmdlineException(message + ": " + value);
}
}

View File

@ -5,16 +5,15 @@ import static net.filebot.util.ExceptionUtilities.*;
import static net.filebot.util.FileUtilities.*;
import java.io.File;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import javax.script.Bindings;
import javax.script.SimpleBindings;
import net.filebot.MediaTypes;
import net.filebot.StandardRenameAction;
public class ArgumentProcessor {
@ -57,47 +56,47 @@ public class ArgumentProcessor {
// print episode info
if (args.list) {
List<String> lines = cli.fetchEpisodeList(args.query, args.format, args.db, args.order, args.filter, args.lang);
List<String> lines = cli.fetchEpisodeList(args.getDatasource(), args.getSearchQuery(), args.getExpressionFormat(), args.getExpressionFilter(), args.getSortOrder(), args.getLanguage().getLocale());
lines.forEach(System.out::println);
return lines.isEmpty() ? 1 : 0;
}
// print media info
if (args.mediaInfo) {
List<String> lines = cli.getMediaInfo(args.getFiles(true), args.format, args.filter);
List<String> lines = cli.getMediaInfo(args.getFiles(true), args.getExpressionFileFilter(), args.getExpressionFormat());
lines.forEach(System.out::println);
return lines.isEmpty() ? 1 : 0;
}
// revert files
if (args.revert) {
List<File> files = cli.revert(args.getFiles(false), args.filter, "TEST".equalsIgnoreCase(args.action));
List<File> files = cli.revert(args.getFiles(false), args.getExpressionFileFilter(), args.getRenameAction());
return files.isEmpty() ? 1 : 0;
}
// file operations
Collection<File> files = new LinkedHashSet<File>(args.getFiles(true));
Set<File> files = new LinkedHashSet<File>(args.getFiles(true));
if (args.extract) {
files.addAll(cli.extract(files, args.output, args.conflict, null, true));
files.addAll(cli.extract(files, args.getOutputPath(), args.getConflictAction(), null, true));
}
if (args.getSubtitles) {
files.addAll(cli.getMissingSubtitles(files, args.db, args.query, args.lang, args.output, args.encoding, args.format, !args.nonStrict));
files.addAll(cli.getMissingSubtitles(files, args.getSearchQuery(), args.getLanguage(), args.getSubtitleOutputFormat(), args.getEncoding(), args.getSubtitleNamingFormat(), args.isStrict()));
}
if (args.rename) {
cli.rename(files, StandardRenameAction.forName(args.action), args.conflict, args.output, args.format, args.db, args.query, args.order, args.filter, args.lang, !args.nonStrict);
cli.rename(files, args.getRenameAction(), args.getConflictAction(), args.getAbsoluteOutputFolder(), args.getExpressionFormat(), args.getDatasource(), args.getSearchQuery(), args.getSortOrder(), args.getExpressionFilter(), args.getLanguage().getLocale(), args.isStrict());
}
if (args.check) {
// check verification file
if (containsOnly(files, MediaTypes.getDefaultFilter("verification"))) {
if (!cli.check(files)) {
throw new Exception("Data corruption detected"); // one or more hashes mismatch
throw new Exception("Data corruption detected"); // one or more hashes do not match
}
} else {
cli.compute(files, args.output, args.encoding);
cli.compute(files, args.getOutputPath(), args.getOutputHashType(), args.getEncoding());
}
}

View File

@ -2,32 +2,42 @@ package net.filebot.cli;
import java.io.File;
import java.io.FileFilter;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import net.filebot.Language;
import net.filebot.RenameAction;
import net.filebot.format.ExpressionFilter;
import net.filebot.format.ExpressionFormat;
import net.filebot.hash.HashType;
import net.filebot.subtitle.SubtitleFormat;
import net.filebot.subtitle.SubtitleNaming;
import net.filebot.web.Datasource;
import net.filebot.web.SortOrder;
public interface CmdlineInterface {
List<File> rename(Collection<File> files, RenameAction action, String conflict, String output, String format, String db, String query, String sortOrder, String filter, String lang, boolean strict) throws Exception;
List<File> rename(Collection<File> files, RenameAction action, ConflictAction conflict, File output, ExpressionFormat format, Datasource db, String query, SortOrder order, ExpressionFilter filter, Locale locale, boolean strict) throws Exception;
List<File> rename(Map<File, File> renameMap, RenameAction renameAction, String conflict) throws Exception;
List<File> rename(Map<File, File> rename, RenameAction action, ConflictAction conflict) throws Exception;
List<File> revert(Collection<File> files, String filter, boolean test) throws Exception;
List<File> revert(Collection<File> files, FileFilter filter, RenameAction action) throws Exception;
List<File> getSubtitles(Collection<File> files, String db, String query, String lang, String output, String encoding, String format, boolean strict) throws Exception;
List<File> getSubtitles(Collection<File> files, String query, Language language, SubtitleFormat output, Charset encoding, SubtitleNaming format, boolean strict) throws Exception;
List<File> getMissingSubtitles(Collection<File> files, String db, String query, String lang, String output, String encoding, String format, boolean strict) throws Exception;
List<File> getMissingSubtitles(Collection<File> files, String query, Language language, SubtitleFormat output, Charset encoding, SubtitleNaming format, boolean strict) throws Exception;
boolean check(Collection<File> files) throws Exception;
File compute(Collection<File> files, String output, String encoding) throws Exception;
File compute(Collection<File> files, File output, HashType hash, Charset encoding) throws Exception;
List<String> fetchEpisodeList(String query, String format, String db, String sortOrder, String filter, String lang) throws Exception;
List<String> fetchEpisodeList(Datasource db, String query, ExpressionFormat format, ExpressionFilter filter, SortOrder order, Locale locale) throws Exception;
List<String> getMediaInfo(Collection<File> files, String format, String filter) throws Exception;
List<String> getMediaInfo(Collection<File> files, FileFilter filter, ExpressionFormat format) throws Exception;
List<File> extract(Collection<File> files, String output, String conflict, FileFilter filter, boolean forceExtractAll) throws Exception;
List<File> extract(Collection<File> files, File output, ConflictAction conflict, FileFilter filter, boolean forceExtractAll) throws Exception;
}

View File

@ -17,7 +17,6 @@ import static net.filebot.util.RegularExpressions.*;
import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
@ -37,7 +36,6 @@ import java.util.Set;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.stream.IntStream;
import java.util.stream.Stream;
@ -49,7 +47,6 @@ import net.filebot.RenameAction;
import net.filebot.StandardRenameAction;
import net.filebot.archive.Archive;
import net.filebot.archive.FileMapper;
import net.filebot.format.ExpressionFileFilter;
import net.filebot.format.ExpressionFilter;
import net.filebot.format.ExpressionFormat;
import net.filebot.format.MediaBindingBean;
@ -59,7 +56,6 @@ import net.filebot.hash.VerificationFileWriter;
import net.filebot.media.AutoDetection;
import net.filebot.media.AutoDetection.Group;
import net.filebot.media.AutoDetection.Type;
import net.filebot.media.MediaDetection;
import net.filebot.media.VideoQuality;
import net.filebot.media.XattrMetaInfoProvider;
import net.filebot.similarity.CommonSequenceMatcher;
@ -93,30 +89,25 @@ import net.filebot.web.VideoHashSubtitleService;
public class CmdlineOperations implements CmdlineInterface {
@Override
public List<File> rename(Collection<File> files, RenameAction action, String conflict, String output, String formatExpression, String db, String query, String sortOrder, String filterExpression, String lang, boolean strict) throws Exception {
ExpressionFormat format = (formatExpression != null) ? new ExpressionFormat(formatExpression) : null;
ExpressionFilter filter = (filterExpression != null) ? new ExpressionFilter(filterExpression) : null;
File outputDir = (output != null && output.length() > 0) ? new File(output).getAbsoluteFile() : null;
Locale locale = getLanguage(lang).getLocale();
ConflictAction conflictAction = ConflictAction.forName(conflict);
if (getMovieIdentificationService(db) != null) {
// movie mode
return renameMovie(files, action, conflictAction, outputDir, format, getMovieIdentificationService(db), query, filter, locale, strict);
public List<File> rename(Collection<File> files, RenameAction action, ConflictAction conflict, File output, ExpressionFormat format, Datasource db, String query, SortOrder order, ExpressionFilter filter, Locale locale, boolean strict) throws Exception {
// movie mode
if (db instanceof MovieIdentificationService) {
return renameMovie(files, action, conflict, output, format, (MovieIdentificationService) db, query, filter, locale, strict);
}
if (getEpisodeListProvider(db) != null) {
// tv series mode
return renameSeries(files, action, conflictAction, outputDir, format, getEpisodeListProvider(db), query, SortOrder.forName(sortOrder), filter, locale, strict);
// series mode
if (db instanceof EpisodeListProvider) {
return renameSeries(files, action, conflict, output, format, (EpisodeListProvider) db, query, order, filter, locale, strict);
}
if (getMusicIdentificationService(db) != null) {
// music mode
return renameMusic(files, action, conflictAction, outputDir, format, getMusicIdentificationService(db));
// music mode
if (db instanceof MusicIdentificationService) {
return renameMusic(files, action, conflict, output, format, (MusicIdentificationService) db);
}
if (XattrMetaData.getIdentifier().equalsIgnoreCase(db)) {
return renameFiles(files, action, conflictAction, outputDir, format, XattrMetaData, filter, strict);
// generic file / xattr mode
if (db instanceof XattrMetaInfoProvider) {
return renameFiles(files, action, conflict, output, format, (XattrMetaInfoProvider) db, filter, strict);
}
// auto-detect mode for each fileset
@ -128,16 +119,16 @@ public class CmdlineOperations implements CmdlineInterface {
for (Type key : it.getKey().types()) {
switch (key) {
case Movie:
results.addAll(renameMovie(it.getValue(), action, conflictAction, outputDir, format, TheMovieDB, query, filter, locale, strict));
results.addAll(renameMovie(it.getValue(), action, conflict, output, format, TheMovieDB, query, filter, locale, strict));
break;
case Series:
results.addAll(renameSeries(it.getValue(), action, conflictAction, outputDir, format, TheTVDB, query, SortOrder.forName(sortOrder), filter, locale, strict));
results.addAll(renameSeries(it.getValue(), action, conflict, output, format, TheTVDB, query, order, filter, locale, strict));
break;
case Anime:
results.addAll(renameSeries(it.getValue(), action, conflictAction, outputDir, format, AniDB, query, SortOrder.forName(sortOrder), filter, locale, strict));
results.addAll(renameSeries(it.getValue(), action, conflict, output, format, AniDB, query, order, filter, locale, strict));
break;
case Music:
results.addAll(renameMusic(it.getValue(), action, conflictAction, outputDir, format, MediaInfoID3, AcoustID));
results.addAll(renameMusic(it.getValue(), action, conflict, output, format, MediaInfoID3, AcoustID));
break;
}
}
@ -146,13 +137,17 @@ public class CmdlineOperations implements CmdlineInterface {
}
}
if (results.isEmpty()) {
throw new CmdlineException("Failed to identify or process any files");
}
return results;
}
@Override
public List<File> rename(Map<File, File> renameMap, RenameAction renameAction, String conflict) throws Exception {
public List<File> rename(Map<File, File> renameMap, RenameAction renameAction, ConflictAction conflict) throws Exception {
// generic rename function that can be passed any set of files
return renameAll(renameMap, renameAction, ConflictAction.forName(conflict), null);
return renameAll(renameMap, renameAction, conflict, null);
}
public List<File> renameSeries(Collection<File> files, RenameAction renameAction, ConflictAction conflictAction, File outputDir, ExpressionFormat format, EpisodeListProvider db, String query, SortOrder sortOrder, ExpressionFilter filter, Locale locale, boolean strict) throws Exception {
@ -658,7 +653,7 @@ public class CmdlineOperations implements CmdlineInterface {
return new ArrayList<File>(renameLog.values());
}
private static File nextAvailableIndexedName(File file) {
protected static File nextAvailableIndexedName(File file) {
File parent = file.getParentFile();
String name = getName(file);
String ext = getExtension(file);
@ -666,17 +661,7 @@ public class CmdlineOperations implements CmdlineInterface {
}
@Override
public List<File> getSubtitles(Collection<File> files, String db, String query, String languageName, String output, String csn, String format, boolean strict) throws Exception {
Language language = getLanguage(languageName);
SubtitleNaming naming = getSubtitleNaming(format);
// use all or only selected subtitle services
Predicate<Datasource> serviceFilter = service -> db == null ? true : db.contains(service.getName()) || db.contains(service.getIdentifier());
// when rewriting subtitles to target format an encoding must be defined, default to UTF-8
Charset outputEncoding = csn != null ? Charset.forName(csn) : output != null ? UTF_8 : null;
SubtitleFormat outputFormat = output != null ? getSubtitleFormatByName(output) : null;
public List<File> getSubtitles(Collection<File> files, String query, Language language, SubtitleFormat output, Charset encoding, SubtitleNaming format, boolean strict) throws Exception {
// ignore anything that is not a video
files = filter(files, VIDEO_FILES);
@ -696,14 +681,14 @@ public class CmdlineOperations implements CmdlineInterface {
// lookup subtitles by hash
for (VideoHashSubtitleService service : getVideoHashSubtitleServices(language.getLocale())) {
if (remainingVideos.isEmpty() || !serviceFilter.test(service) || !requireLogin(service)) {
if (remainingVideos.isEmpty() || !requireLogin(service)) {
continue;
}
try {
log.fine("Looking up subtitles by hash via " + service.getName());
Map<File, List<SubtitleDescriptor>> options = lookupSubtitlesByHash(service, remainingVideos, language.getName(), false, strict);
Map<File, File> downloads = downloadSubtitleBatch(service, options, outputFormat, outputEncoding, naming);
Map<File, File> downloads = downloadSubtitleBatch(service, options, output, encoding, format);
remainingVideos.removeAll(downloads.keySet());
subtitleFiles.addAll(downloads.values());
} catch (Exception e) {
@ -711,15 +696,15 @@ public class CmdlineOperations implements CmdlineInterface {
}
}
for (SubtitleProvider service : getSubtitleProviders()) {
if (strict || remainingVideos.isEmpty() || !serviceFilter.test(service) || !requireLogin(service)) {
for (SubtitleProvider service : getSubtitleProviders(language.getLocale())) {
if (strict || remainingVideos.isEmpty() || !requireLogin(service)) {
continue;
}
try {
log.fine(format("Looking up subtitles by name via %s", service.getName()));
Map<File, List<SubtitleDescriptor>> options = findSubtitlesByName(service, remainingVideos, language.getName(), query, false, strict);
Map<File, File> downloads = downloadSubtitleBatch(service, options, outputFormat, outputEncoding, naming);
Map<File, File> downloads = downloadSubtitleBatch(service, options, output, encoding, format);
remainingVideos.removeAll(downloads.keySet());
subtitleFiles.addAll(downloads.values());
} catch (Exception e) {
@ -735,7 +720,7 @@ public class CmdlineOperations implements CmdlineInterface {
return subtitleFiles;
}
private static boolean requireLogin(Object service) {
protected static boolean requireLogin(Object service) {
if (service instanceof OpenSubtitlesClient) {
OpenSubtitlesClient osdb = (OpenSubtitlesClient) service;
if (osdb.isAnonymous()) {
@ -746,47 +731,38 @@ public class CmdlineOperations implements CmdlineInterface {
}
@Override
public List<File> getMissingSubtitles(Collection<File> files, String db, String query, String languageName, String output, String csn, String format, boolean strict) throws Exception {
// sanity check
for (File f : files) {
if (!f.exists()) {
throw new FileNotFoundException(f.toString());
}
}
public List<File> getMissingSubtitles(Collection<File> files, String query, Language language, SubtitleFormat output, Charset encoding, SubtitleNaming format, boolean strict) throws Exception {
List<File> videoFiles = filter(filter(files, VIDEO_FILES), new FileFilter() {
// save time on repeating filesystem calls
private Map<File, List<File>> cache = new HashMap<File, List<File>>();
private SubtitleNaming naming = getSubtitleNaming(format);
// get language code suffix for given language (.eng)
private String languageCode = Language.getStandardLanguageCode(getLanguage(languageName).getName());
public boolean matchesLanguageCode(File f) {
Locale languageSuffix = MediaDetection.releaseInfo.getSubtitleLanguageTag(getName(f));
Language language = Language.getLanguage(languageSuffix);
if (language != null) {
return language.getISO3().equalsIgnoreCase(languageCode);
Language languageSuffix = Language.getLanguage(releaseInfo.getSubtitleLanguageTag(getName(f)));
if (languageSuffix != null) {
return languageSuffix.getCode().equals(language.getCode());
}
return false;
}
@Override
public boolean accept(File video) {
if (!video.isFile()) {
return false;
}
List<File> subtitleFiles = cache.computeIfAbsent(video.getParentFile(), parent -> {
return getChildren(parent, SUBTITLE_FILES);
});
// can't tell which subtitle belongs to which file -> if any subtitles exist skip the whole folder
if (naming == SubtitleNaming.ORIGINAL) {
if (format == SubtitleNaming.ORIGINAL) {
return subtitleFiles.size() == 0;
}
return subtitleFiles.stream().allMatch(f -> {
if (isDerived(f, video)) {
return naming != SubtitleNaming.MATCH_VIDEO && !matchesLanguageCode(f);
return format != SubtitleNaming.MATCH_VIDEO && !matchesLanguageCode(f);
}
return true;
});
@ -798,16 +774,7 @@ public class CmdlineOperations implements CmdlineInterface {
return emptyList();
}
return getSubtitles(videoFiles, db, query, languageName, output, csn, format, strict);
}
private SubtitleNaming getSubtitleNaming(String format) {
SubtitleNaming naming = SubtitleNaming.forName(format);
if (naming != null) {
return naming;
} else {
return SubtitleNaming.MATCH_VIDEO_ADD_LANGUAGE_TAG;
}
return getSubtitles(videoFiles, query, language, output, encoding, format, strict);
}
private Map<File, File> downloadSubtitleBatch(Datasource service, Map<File, List<SubtitleDescriptor>> subtitles, SubtitleFormat outputFormat, Charset outputEncoding, SubtitleNaming naming) {
@ -834,19 +801,25 @@ public class CmdlineOperations implements CmdlineInterface {
MemoryFile subtitleFile = fetchSubtitle(descriptor);
// subtitle filename is based on movie filename
String ext = getExtension(subtitleFile.getName());
String extension = getExtension(subtitleFile.getName());
ByteBuffer data = subtitleFile.getData();
if (outputFormat != null || outputEncoding != null) {
// adjust extension of the output file
if (outputFormat != null) {
ext = outputFormat.getFilter().extension(); // adjust extension of the output file
extension = outputFormat.getFilter().extension();
}
log.finest(format("Export [%s] as [%s / %s]", subtitleFile.getName(), outputFormat, outputEncoding.displayName(Locale.ROOT)));
// default to UTF-8 if no other encoding is given
if (outputEncoding == null) {
outputEncoding = UTF_8;
}
log.finest(format("Export [%s] as [%s / %s]", subtitleFile.getName(), outputFormat, outputEncoding));
data = exportSubtitles(subtitleFile, outputFormat, 0, outputEncoding);
}
File destination = new File(movieFile.getParentFile(), naming.format(movieFile, descriptor, ext));
File destination = new File(movieFile.getParentFile(), naming.format(movieFile, descriptor, extension));
log.info(format("Writing [%s] to [%s]", subtitleFile.getName(), destination.getName()));
writeFile(data, destination);
@ -893,18 +866,6 @@ public class CmdlineOperations implements CmdlineInterface {
return probableMatches.size() <= 5 ? probableMatches : probableMatches.subList(0, 5); // trust that the correct match is in the Top 3
}
private Language getLanguage(String lang) throws Exception {
// try to look up by language code
Language language = Language.findLanguage(lang);
if (language == null) {
// unable to lookup language
throw new CmdlineException("Illegal language code: " + lang);
}
return language;
}
@Override
public boolean check(Collection<File> files) throws Exception {
// only check existing hashes
@ -918,10 +879,14 @@ public class CmdlineOperations implements CmdlineInterface {
}
@Override
public File compute(Collection<File> files, String output, String csn) throws Exception {
public File compute(Collection<File> files, File output, HashType hash, Charset encoding) throws Exception {
// ignore folders and any sort of special files
files = filter(files, FILES);
if (files.isEmpty()) {
throw new CmdlineException("No files: " + files);
}
// find common parent folder of all files
File[] fileList = files.toArray(new File[0]);
File[][] pathArray = new File[fileList.length][];
@ -933,38 +898,22 @@ public class CmdlineOperations implements CmdlineInterface {
File[] common = csm.matchFirstCommonSequence(pathArray);
if (common == null) {
throw new CmdlineException("Paths must be on the same filesystem: " + files);
throw new CmdlineException("All paths must be on the same filesystem: " + files);
}
// last element in the common sequence must be the root folder
File root = common[common.length - 1];
// create verification file
File outputFile;
HashType hashType;
if (output != null && getExtension(output) != null) {
// use given filename
hashType = getHashTypeByExtension(getExtension(output));
outputFile = new File(root, output);
} else {
// auto-select the filename based on folder and type
hashType = (output != null) ? getHashTypeByExtension(output) : HashType.SFV;
outputFile = new File(root, root.getName() + "." + hashType.getFilter().extension());
if (output == null) {
output = new File(root, root.getName() + '.' + hash.getFilter().extension());
} else if (!output.isAbsolute()) {
output = new File(root, output.getPath());
}
if (hashType == null) {
throw new CmdlineException("Illegal output type: " + output);
}
log.info(format("Compute %s hash for %s files [%s]", hash, files.size(), output));
compute(root, files, output, hash, encoding);
if (files.isEmpty()) {
throw new CmdlineException("No files: " + files);
}
log.info(format("Compute %s hash for %s files [%s]", hashType, files.size(), outputFile));
compute(root.getPath(), files, outputFile, hashType, csn);
return outputFile;
return output;
}
private boolean check(File verificationFile, File root) throws Exception {
@ -1004,16 +953,16 @@ public class CmdlineOperations implements CmdlineInterface {
return status;
}
private void compute(String root, Collection<File> files, File outputFile, HashType hashType, String csn) throws IOException, Exception {
private void compute(File root, Collection<File> files, File outputFile, HashType hashType, Charset encoding) throws IOException, Exception {
// compute hashes recursively and write to file
VerificationFileWriter out = new VerificationFileWriter(outputFile, hashType.getFormat(), csn != null ? csn : "UTF-8");
VerificationFileWriter out = new VerificationFileWriter(outputFile, hashType.getFormat(), encoding != null ? encoding : UTF_8);
try {
for (File it : files) {
if (it.isHidden() || MediaTypes.getDefaultFilter("verification").accept(it))
continue;
String relativePath = normalizePathSeparators(it.getPath().replace(root, "")).substring(1);
String relativePath = normalizePathSeparators(it.getPath().substring(root.getPath().length() + 1)); // skip root and first slash
String hash = computeHash(it, hashType);
log.info(format("%s %s", hash, relativePath));
@ -1028,17 +977,13 @@ public class CmdlineOperations implements CmdlineInterface {
}
@Override
public List<String> fetchEpisodeList(String query, String expression, String db, String sortOrderName, String filterExpression, String languageName) throws Exception {
if (query == null || query.isEmpty()) {
public List<String> fetchEpisodeList(Datasource db, String query, ExpressionFormat format, ExpressionFilter filter, SortOrder order, Locale locale) throws Exception {
if (query == null) {
throw new IllegalArgumentException("query is not defined");
}
// find series on the web and fetch episode list
ExpressionFormat format = expression == null ? null : new ExpressionFormat(expression);
ExpressionFilter filter = filterExpression == null ? null : new ExpressionFilter(filterExpression);
EpisodeListProvider service = db == null ? TheTVDB : getEpisodeListProvider(db);
SortOrder sortOrder = SortOrder.forName(sortOrderName);
Locale locale = getLanguage(languageName).getLocale();
EpisodeListProvider service = db instanceof EpisodeListProvider ? (EpisodeListProvider) db : TheTVDB;
// search and select search result
List<SearchResult> options = selectSearchResult(query, service.search(query, locale), false, false);
@ -1047,7 +992,7 @@ public class CmdlineOperations implements CmdlineInterface {
}
// fetch episodes and apply filter
List<Episode> episodes = applyExpressionFilter(service.getEpisodeList(options.get(0), sortOrder, locale), filter);
List<Episode> episodes = applyExpressionFilter(service.getEpisodeList(options.get(0), order, locale), filter);
Map<File, Episode> context = new EntryList<File, Episode>(null, episodes);
return episodes.stream().map(episode -> {
@ -1056,49 +1001,52 @@ public class CmdlineOperations implements CmdlineInterface {
}
@Override
public List<String> getMediaInfo(Collection<File> files, String format, String filter) throws Exception {
ExpressionFormat formatter = new ExpressionFormat(format != null && format.length() > 0 ? format : "{fn} [{resolution} {vc} {channels} {ac} {minutes}m]");
List<File> selection = filter(files, filter == null || filter.isEmpty() ? f -> true : new ExpressionFileFilter(new ExpressionFilter(filter), false));
public List<String> getMediaInfo(Collection<File> files, FileFilter filter, ExpressionFormat format) throws Exception {
List<File> selection = filter(files, FILES);
// apply custom filter
if (filter != null) {
selection = filter(selection, filter);
}
// default expression format if not set
ExpressionFormat formatter = format != null ? format : new ExpressionFormat("{fn} [{resolution} {vc} {channels} {ac} {minutes}m]");
// lazy format
return new FunctionList<File, String>(selection, f -> formatter.format(new MediaBindingBean(xattr.getMetaInfo(f), f, null)));
}
@Override
public List<File> revert(Collection<File> files, String filter, boolean test) throws Exception {
public List<File> revert(Collection<File> files, FileFilter filter, RenameAction action) throws Exception {
if (files.isEmpty()) {
throw new CmdlineException("Expecting at least one input path");
}
FileFilter fileFilter = filter == null || filter.isEmpty() ? f -> true : new ExpressionFileFilter(new ExpressionFilter(filter), false);
Set<File> whitelist = new HashSet<File>(files);
Map<File, File> history = HistorySpooler.getInstance().getCompleteHistory().getRenameMap();
return history.entrySet().stream().filter(it -> {
File original = it.getKey();
File current = it.getValue();
return Stream.of(current, original).flatMap(f -> listPath(f).stream()).anyMatch(whitelist::contains) && current.exists() && fileFilter.accept(current);
return Stream.of(current, original).flatMap(f -> listPath(f).stream()).anyMatch(whitelist::contains) && current.exists() && (filter == null || filter.accept(current));
}).map(it -> {
File original = it.getKey();
File current = it.getValue();
log.info(format("Revert [%s] to [%s]", current, original));
if (test) {
return original;
}
try {
return StandardRenameAction.revert(current, original);
} catch (Exception e) {
log.warning(format("Failed to revert file: %s", e.getMessage()));
return null;
if (action.canRevert()) {
try {
return StandardRenameAction.revert(current, original);
} catch (Exception e) {
log.warning("Failed to revert file: " + e);
}
}
return null;
}).filter(Objects::nonNull).collect(toList());
}
@Override
public List<File> extract(Collection<File> files, String output, String conflict, FileFilter filter, boolean forceExtractAll) throws Exception {
ConflictAction conflictAction = ConflictAction.forName(conflict);
public List<File> extract(Collection<File> files, File output, ConflictAction conflict, FileFilter filter, boolean forceExtractAll) throws Exception {
// only keep single-volume archives or first part of multi-volume archives
List<File> archiveFiles = filter(files, Archive.VOLUME_ONE_FILTER);
List<File> extractedFiles = new ArrayList<File>();
@ -1106,11 +1054,11 @@ public class CmdlineOperations implements CmdlineInterface {
for (File file : archiveFiles) {
Archive archive = Archive.open(file);
try {
File outputFolder = new File(output != null ? output : getName(file));
if (!outputFolder.isAbsolute()) {
outputFolder = new File(file.getParentFile(), outputFolder.getPath());
File outputFolder = output;
if (outputFolder == null || !outputFolder.isAbsolute()) {
outputFolder = new File(file.getParentFile(), outputFolder == null ? getName(file) : outputFolder.getPath()).getCanonicalFile();
}
outputFolder = outputFolder.getCanonicalFile(); // normalize weird paths
log.info(format("Read archive [%s] and extract to [%s]", file.getName(), outputFolder));
FileMapper outputMapper = new FileMapper(outputFolder);
@ -1135,14 +1083,14 @@ public class CmdlineOperations implements CmdlineInterface {
boolean skip = true;
for (FileInfo future : filter == null || forceExtractAll ? outputMapping : selection) {
if (conflictAction == ConflictAction.AUTO) {
if (conflict == ConflictAction.AUTO) {
skip &= (future.toFile().exists() && future.getLength() == future.toFile().length());
} else {
skip &= (future.toFile().exists());
}
}
if (!skip || conflictAction == ConflictAction.OVERRIDE) {
if (!skip || conflict == ConflictAction.OVERRIDE) {
if (filter == null || forceExtractAll) {
log.finest("Extracting files " + outputMapping);

View File

@ -6,14 +6,14 @@ import static net.filebot.media.XattrMetaInfo.*;
import java.io.File;
import java.io.FileFilter;
import javax.script.ScriptException;
public class ExpressionFileFilter implements FileFilter {
private final ExpressionFilter filter;
private final boolean error;
private ExpressionFilter filter;
public ExpressionFileFilter(ExpressionFilter filter, boolean error) {
this.filter = filter;
this.error = error;
public ExpressionFileFilter(String expression) throws ScriptException {
this.filter = new ExpressionFilter(expression);
}
public ExpressionFilter getExpressionFilter() {
@ -25,9 +25,9 @@ public class ExpressionFileFilter implements FileFilter {
try {
return filter.matches(new MediaBindingBean(xattr.getMetaInfo(f), f, null));
} catch (Exception e) {
debug.warning(format("Expression failed: %s", e));
return error;
debug.warning("Filter expression failed: " + e);
}
return false;
}
}

View File

@ -24,7 +24,6 @@ public enum HashType {
public ExtensionFileFilter getFilter() {
return MediaTypes.getDefaultFilter("verification/sfv");
}
},
MD5 {
@ -44,7 +43,6 @@ public enum HashType {
public ExtensionFileFilter getFilter() {
return MediaTypes.getDefaultFilter("verification/md5sum");
}
},
SHA1 {
@ -69,7 +67,6 @@ public enum HashType {
public String toString() {
return "SHA1";
}
},
SHA256 {
@ -94,7 +91,6 @@ public enum HashType {
public String toString() {
return "SHA2";
}
},
ED2K {

View File

@ -1,28 +1,28 @@
package net.filebot.hash;
import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.util.Date;
import net.filebot.Settings;
public class VerificationFileWriter implements Closeable {
protected PrintWriter out;
protected VerificationFormat format;
public VerificationFileWriter(File file, VerificationFormat format, String charset) throws IOException {
this(new PrintWriter(file, charset), format, charset);
public VerificationFileWriter(File file, VerificationFormat format, Charset charset) throws IOException {
this(new PrintWriter(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), charset)), false), format, charset);
}
public VerificationFileWriter(PrintWriter out, VerificationFormat format, String charset) {
public VerificationFileWriter(PrintWriter out, VerificationFormat format, Charset charset) {
this.out = out;
this.format = format;
@ -30,19 +30,16 @@ public class VerificationFileWriter implements Closeable {
writeHeader(charset);
}
protected void writeHeader(String charset) {
protected void writeHeader(Charset charset) {
out.format("; Generated by %s %s on %tF at %<tT%n", Settings.getApplicationName(), Settings.getApplicationVersion(), new Date());
out.format("; charset=%s%n", charset);
out.format(";%n");
}
public void write(String path, String hash) {
out.format("%s%n", format.format(path, hash));
}
@Override
public void close() throws IOException {
out.close();

View File

@ -57,7 +57,7 @@ public class Preset {
}
public ExpressionFileFilter getIncludeFilter() {
return getInputFolder() == null ? null : getValue(includes, expression -> new ExpressionFileFilter(new ExpressionFilter(expression), false));
return getInputFolder() == null ? null : getValue(includes, expression -> new ExpressionFileFilter(expression));
}
public ExpressionFormat getFormat() {

View File

@ -1,6 +1,7 @@
package net.filebot.ui.sfv;
import static java.nio.charset.StandardCharsets.*;
import java.io.File;
import java.io.IOException;
@ -11,35 +12,29 @@ import net.filebot.hash.VerificationFileWriter;
import net.filebot.ui.transfer.TextFileExportHandler;
import net.filebot.util.FileUtilities;
class ChecksumTableExportHandler extends TextFileExportHandler {
private final ChecksumTableModel model;
public ChecksumTableExportHandler(ChecksumTableModel model) {
this.model = model;
}
@Override
public boolean canExport() {
return model.getRowCount() > 0 && defaultColumn() != null;
}
@Override
public void export(PrintWriter out) {
export(new VerificationFileWriter(out, model.getHashType().getFormat(), "UTF-8"), defaultColumn(), model.getHashType());
export(new VerificationFileWriter(out, model.getHashType().getFormat(), UTF_8), defaultColumn(), model.getHashType());
}
@Override
public String getDefaultFileName() {
return getDefaultFileName(defaultColumn());
}
protected File defaultColumn() {
// select first column that is not a verification file column
for (File root : model.getChecksumColumns()) {
@ -50,9 +45,8 @@ class ChecksumTableExportHandler extends TextFileExportHandler {
return null;
}
public void export(File file, File column) throws IOException {
VerificationFileWriter writer = new VerificationFileWriter(file, model.getHashType().getFormat(), "UTF-8");
VerificationFileWriter writer = new VerificationFileWriter(file, model.getHashType().getFormat(), UTF_8);
try {
export(writer, column, model.getHashType());
@ -61,7 +55,6 @@ class ChecksumTableExportHandler extends TextFileExportHandler {
}
}
public void export(VerificationFileWriter out, File column, HashType type) {
for (ChecksumRow row : model.rows()) {
ChecksumCell cell = row.getChecksum(column);
@ -76,7 +69,6 @@ class ChecksumTableExportHandler extends TextFileExportHandler {
}
}
public String getDefaultFileName(File column) {
StringBuilder sb = new StringBuilder();

View File

@ -124,15 +124,18 @@ public class SubtitlePanel extends AbstractSearchPanel<SubtitleProvider, Subtitl
private final SubtitleDropTarget downloadDropTarget = new SubtitleDropTarget.Download() {
public Locale getLocale() {
return languageComboBox.getModel().getSelectedItem() == ALL_LANGUAGES ? Locale.ROOT : languageComboBox.getModel().getSelectedItem().getLocale();
}
@Override
public VideoHashSubtitleService[] getVideoHashSubtitleServices() {
Locale locale = languageComboBox.getModel().getSelectedItem() == ALL_LANGUAGES ? Locale.ROOT : languageComboBox.getModel().getSelectedItem().getLocale();
return WebServices.getVideoHashSubtitleServices(locale);
return WebServices.getVideoHashSubtitleServices(getLocale());
}
@Override
public SubtitleProvider[] getSubtitleProviders() {
return WebServices.getSubtitleProviders();
return WebServices.getSubtitleProviders(getLocale());
}
@Override
@ -182,7 +185,7 @@ public class SubtitlePanel extends AbstractSearchPanel<SubtitleProvider, Subtitl
@Override
protected SubtitleProvider[] getSearchEngines() {
return WebServices.getSubtitleProviders();
return WebServices.getSubtitleProviders(getLocale());
}
@Override