diff --git a/source/net/filebot/WebServices.java b/source/net/filebot/WebServices.java index 5fb3f58e..96de0e07 100644 --- a/source/net/filebot/WebServices.java +++ b/source/net/filebot/WebServices.java @@ -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 getService(String name, T[] services) { - return StreamEx.of(services).findFirst(it -> it.getIdentifier().equalsIgnoreCase(name) || it.getName().equalsIgnoreCase(name)).orElse(null); + public static 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 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> localSearch = requestThreadPool.submit(() -> localIndex.get().search(query)); // combine alias names into a single search results, and keep API search name as primary name - Map results = StreamEx.of(apiSearch.get()).append(localSearch.get()).groupingBy(SearchResult::getId, collectingAndThen(toList(), group -> merge(group.get(0), group))); + Map 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()); } diff --git a/source/net/filebot/cli/ArgumentBean.java b/source/net/filebot/cli/ArgumentBean.java index 3da198cb..17b93ad4 100644 --- a/source/net/filebot/cli/ArgumentBean.java +++ b/source/net/filebot/cli/ArgumentBean.java @@ -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 Optional optional(T value) { + return Optional.ofNullable(value); + } + + private static Supplier error(String message, Object value) { + return () -> new CmdlineException(message + ": " + value); + } + } diff --git a/source/net/filebot/cli/ArgumentProcessor.java b/source/net/filebot/cli/ArgumentProcessor.java index a85d36df..2c9640f8 100644 --- a/source/net/filebot/cli/ArgumentProcessor.java +++ b/source/net/filebot/cli/ArgumentProcessor.java @@ -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 lines = cli.fetchEpisodeList(args.query, args.format, args.db, args.order, args.filter, args.lang); + List 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 lines = cli.getMediaInfo(args.getFiles(true), args.format, args.filter); + List 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 files = cli.revert(args.getFiles(false), args.filter, "TEST".equalsIgnoreCase(args.action)); + List files = cli.revert(args.getFiles(false), args.getExpressionFileFilter(), args.getRenameAction()); return files.isEmpty() ? 1 : 0; } // file operations - Collection files = new LinkedHashSet(args.getFiles(true)); + Set files = new LinkedHashSet(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()); } } diff --git a/source/net/filebot/cli/CmdlineInterface.java b/source/net/filebot/cli/CmdlineInterface.java index e9ee8167..cd48f977 100644 --- a/source/net/filebot/cli/CmdlineInterface.java +++ b/source/net/filebot/cli/CmdlineInterface.java @@ -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 rename(Collection files, RenameAction action, String conflict, String output, String format, String db, String query, String sortOrder, String filter, String lang, boolean strict) throws Exception; + List rename(Collection files, RenameAction action, ConflictAction conflict, File output, ExpressionFormat format, Datasource db, String query, SortOrder order, ExpressionFilter filter, Locale locale, boolean strict) throws Exception; - List rename(Map renameMap, RenameAction renameAction, String conflict) throws Exception; + List rename(Map rename, RenameAction action, ConflictAction conflict) throws Exception; - List revert(Collection files, String filter, boolean test) throws Exception; + List revert(Collection files, FileFilter filter, RenameAction action) throws Exception; - List getSubtitles(Collection files, String db, String query, String lang, String output, String encoding, String format, boolean strict) throws Exception; + List getSubtitles(Collection files, String query, Language language, SubtitleFormat output, Charset encoding, SubtitleNaming format, boolean strict) throws Exception; - List getMissingSubtitles(Collection files, String db, String query, String lang, String output, String encoding, String format, boolean strict) throws Exception; + List getMissingSubtitles(Collection files, String query, Language language, SubtitleFormat output, Charset encoding, SubtitleNaming format, boolean strict) throws Exception; boolean check(Collection files) throws Exception; - File compute(Collection files, String output, String encoding) throws Exception; + File compute(Collection files, File output, HashType hash, Charset encoding) throws Exception; - List fetchEpisodeList(String query, String format, String db, String sortOrder, String filter, String lang) throws Exception; + List fetchEpisodeList(Datasource db, String query, ExpressionFormat format, ExpressionFilter filter, SortOrder order, Locale locale) throws Exception; - List getMediaInfo(Collection files, String format, String filter) throws Exception; + List getMediaInfo(Collection files, FileFilter filter, ExpressionFormat format) throws Exception; - List extract(Collection files, String output, String conflict, FileFilter filter, boolean forceExtractAll) throws Exception; + List extract(Collection files, File output, ConflictAction conflict, FileFilter filter, boolean forceExtractAll) throws Exception; } diff --git a/source/net/filebot/cli/CmdlineOperations.java b/source/net/filebot/cli/CmdlineOperations.java index 6610c475..3949583d 100644 --- a/source/net/filebot/cli/CmdlineOperations.java +++ b/source/net/filebot/cli/CmdlineOperations.java @@ -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 rename(Collection 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 rename(Collection 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 rename(Map renameMap, RenameAction renameAction, String conflict) throws Exception { + public List rename(Map 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 renameSeries(Collection 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(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 getSubtitles(Collection 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 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 getSubtitles(Collection 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> options = lookupSubtitlesByHash(service, remainingVideos, language.getName(), false, strict); - Map downloads = downloadSubtitleBatch(service, options, outputFormat, outputEncoding, naming); + Map 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> options = findSubtitlesByName(service, remainingVideos, language.getName(), query, false, strict); - Map downloads = downloadSubtitleBatch(service, options, outputFormat, outputEncoding, naming); + Map 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 getMissingSubtitles(Collection 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 getMissingSubtitles(Collection files, String query, Language language, SubtitleFormat output, Charset encoding, SubtitleNaming format, boolean strict) throws Exception { List videoFiles = filter(filter(files, VIDEO_FILES), new FileFilter() { // save time on repeating filesystem calls private Map> cache = new HashMap>(); - 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 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 downloadSubtitleBatch(Datasource service, Map> 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 files) throws Exception { // only check existing hashes @@ -918,10 +879,14 @@ public class CmdlineOperations implements CmdlineInterface { } @Override - public File compute(Collection files, String output, String csn) throws Exception { + public File compute(Collection 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 files, File outputFile, HashType hashType, String csn) throws IOException, Exception { + private void compute(File root, Collection 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 fetchEpisodeList(String query, String expression, String db, String sortOrderName, String filterExpression, String languageName) throws Exception { - if (query == null || query.isEmpty()) { + public List 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 options = selectSearchResult(query, service.search(query, locale), false, false); @@ -1047,7 +992,7 @@ public class CmdlineOperations implements CmdlineInterface { } // fetch episodes and apply filter - List episodes = applyExpressionFilter(service.getEpisodeList(options.get(0), sortOrder, locale), filter); + List episodes = applyExpressionFilter(service.getEpisodeList(options.get(0), order, locale), filter); Map context = new EntryList(null, episodes); return episodes.stream().map(episode -> { @@ -1056,49 +1001,52 @@ public class CmdlineOperations implements CmdlineInterface { } @Override - public List getMediaInfo(Collection 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 selection = filter(files, filter == null || filter.isEmpty() ? f -> true : new ExpressionFileFilter(new ExpressionFilter(filter), false)); + public List getMediaInfo(Collection files, FileFilter filter, ExpressionFormat format) throws Exception { + List 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(selection, f -> formatter.format(new MediaBindingBean(xattr.getMetaInfo(f), f, null))); } @Override - public List revert(Collection files, String filter, boolean test) throws Exception { + public List revert(Collection 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 whitelist = new HashSet(files); Map 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 extract(Collection files, String output, String conflict, FileFilter filter, boolean forceExtractAll) throws Exception { - ConflictAction conflictAction = ConflictAction.forName(conflict); - + public List extract(Collection files, File output, ConflictAction conflict, FileFilter filter, boolean forceExtractAll) throws Exception { // only keep single-volume archives or first part of multi-volume archives List archiveFiles = filter(files, Archive.VOLUME_ONE_FILTER); List extractedFiles = new ArrayList(); @@ -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); diff --git a/source/net/filebot/format/ExpressionFileFilter.java b/source/net/filebot/format/ExpressionFileFilter.java index fd46ac73..87343264 100644 --- a/source/net/filebot/format/ExpressionFileFilter.java +++ b/source/net/filebot/format/ExpressionFileFilter.java @@ -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; } } diff --git a/source/net/filebot/hash/HashType.java b/source/net/filebot/hash/HashType.java index 47c35652..798a9421 100644 --- a/source/net/filebot/hash/HashType.java +++ b/source/net/filebot/hash/HashType.java @@ -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 { diff --git a/source/net/filebot/hash/VerificationFileWriter.java b/source/net/filebot/hash/VerificationFileWriter.java index e4bca6e4..df6016ff 100644 --- a/source/net/filebot/hash/VerificationFileWriter.java +++ b/source/net/filebot/hash/VerificationFileWriter.java @@ -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 % new ExpressionFileFilter(new ExpressionFilter(expression), false)); + return getInputFolder() == null ? null : getValue(includes, expression -> new ExpressionFileFilter(expression)); } public ExpressionFormat getFormat() { diff --git a/source/net/filebot/ui/sfv/ChecksumTableExportHandler.java b/source/net/filebot/ui/sfv/ChecksumTableExportHandler.java index 1b574c64..531f6227 100644 --- a/source/net/filebot/ui/sfv/ChecksumTableExportHandler.java +++ b/source/net/filebot/ui/sfv/ChecksumTableExportHandler.java @@ -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(); diff --git a/source/net/filebot/ui/subtitle/SubtitlePanel.java b/source/net/filebot/ui/subtitle/SubtitlePanel.java index 1b5535fa..ffaf38b9 100644 --- a/source/net/filebot/ui/subtitle/SubtitlePanel.java +++ b/source/net/filebot/ui/subtitle/SubtitlePanel.java @@ -124,15 +124,18 @@ public class SubtitlePanel extends AbstractSearchPanel