From 01ec6309cc168859c6896b28e85fe38a4a272e85 Mon Sep 17 00:00:00 2001 From: Reinhard Pointner Date: Tue, 13 Sep 2011 18:16:38 +0000 Subject: [PATCH] * ability to check/compute sfv/md5/sha1 via CLI * ability to auto-detect Series/Movie in rename CLI (force Series or Movie mode by setting episode/movie db) * CLI -non-strict setting for renameSeries * ignore hidden files whenever listing files via FileUtilities * misc. refactoring --- source/net/sourceforge/filebot/Main.java | 34 +-- .../sourceforge/filebot/Settings.properties | 2 +- .../net/sourceforge/filebot/WebServices.java | 30 ++ .../sourceforge/filebot/cli/ArgumentBean.java | 94 +++--- .../filebot/cli/ArgumentProcessor.java | 276 ++++++++++++++---- .../sourceforge/filebot/cli/CLILogging.java | 3 + .../filebot/hash/VerificationFileWriter.java | 51 ++++ .../filebot/hash/VerificationUtilities.java | 10 + .../list/FileListTransferablePolicy.java | 2 +- .../rename/FilesListTransferablePolicy.java | 2 +- .../rename/NamesListTransferablePolicy.java | 2 +- .../filebot/ui/panel/rename/RenameAction.java | 2 +- .../filebot/ui/panel/rename/RenamePanel.java | 12 +- .../panel/sfv/ChecksumTableExportHandler.java | 28 +- .../sfv/ChecksumTableTransferablePolicy.java | 25 +- .../ui/panel/subtitle/SubtitleDropTarget.java | 4 +- .../web/MovieIdentificationService.java | 5 + .../sourceforge/filebot/web/TMDbClient.java | 2 + .../net/sourceforge/tuned/FileUtilities.java | 22 +- 19 files changed, 433 insertions(+), 173 deletions(-) create mode 100644 source/net/sourceforge/filebot/hash/VerificationFileWriter.java diff --git a/source/net/sourceforge/filebot/Main.java b/source/net/sourceforge/filebot/Main.java index bac58e36..fcfbc6ab 100644 --- a/source/net/sourceforge/filebot/Main.java +++ b/source/net/sourceforge/filebot/Main.java @@ -20,6 +20,7 @@ import javax.swing.JFrame; import javax.swing.SwingUtilities; import javax.swing.UIManager; +import org.kohsuke.args4j.CmdLineException; import org.kohsuke.args4j.CmdLineParser; import net.sf.ehcache.CacheManager; @@ -43,7 +44,19 @@ public class Main { initializeSecurityManager(); // parse arguments - final ArgumentBean argumentBean = initializeArgumentBean(args); + final ArgumentBean argumentBean = new ArgumentBean(); + + if (args != null && args.length > 0) { + try { + CmdLineParser parser = new CmdLineParser(argumentBean); + parser.parseArgument(args); + } catch (CmdLineException e) { + System.out.println(e.getMessage()); + + // just print CLI error message and stop + System.exit(-1); + } + } if (argumentBean.printHelp()) { new CmdLineParser(argumentBean).printUsage(System.out); @@ -180,23 +193,4 @@ public class Main { } } - - /** - * Parse command line arguments. - */ - private static ArgumentBean initializeArgumentBean(String... args) { - ArgumentBean argumentBean = new ArgumentBean(); - - if (args != null && args.length > 0) { - try { - CmdLineParser parser = new CmdLineParser(argumentBean); - parser.parseArgument(args); - } catch (Exception e) { - Logger.getLogger(Main.class.getName()).log(Level.WARNING, e.getMessage(), e); - } - } - - return argumentBean; - } - } diff --git a/source/net/sourceforge/filebot/Settings.properties b/source/net/sourceforge/filebot/Settings.properties index 406c52e9..cacdd593 100644 --- a/source/net/sourceforge/filebot/Settings.properties +++ b/source/net/sourceforge/filebot/Settings.properties @@ -1,6 +1,6 @@ # application settings application.name: FileBot -application.version: 1.98 +application.version: 1.99 thetvdb.apikey: 58B4AA94C59AD656 themoviedb.apikey: 5a6edae568130bf10617b6d45be99f13 diff --git a/source/net/sourceforge/filebot/WebServices.java b/source/net/sourceforge/filebot/WebServices.java index 54fcb2ed..07e8a946 100644 --- a/source/net/sourceforge/filebot/WebServices.java +++ b/source/net/sourceforge/filebot/WebServices.java @@ -7,11 +7,13 @@ import static net.sourceforge.filebot.Settings.*; import net.sourceforge.filebot.web.AnidbClient; import net.sourceforge.filebot.web.EpisodeListProvider; import net.sourceforge.filebot.web.IMDbClient; +import net.sourceforge.filebot.web.MovieIdentificationService; import net.sourceforge.filebot.web.OpenSubtitlesClient; import net.sourceforge.filebot.web.SerienjunkiesClient; import net.sourceforge.filebot.web.SublightSubtitleClient; import net.sourceforge.filebot.web.SubsceneSubtitleClient; import net.sourceforge.filebot.web.SubtitleProvider; +import net.sourceforge.filebot.web.TMDbClient; import net.sourceforge.filebot.web.TVRageClient; import net.sourceforge.filebot.web.TheTVDBClient; import net.sourceforge.filebot.web.VideoHashSubtitleService; @@ -34,12 +36,20 @@ public final class WebServices { public static final SublightSubtitleClient Sublight = new SublightSubtitleClient(getApplicationName(), getApplicationProperty("sublight.apikey")); public static final SubsceneSubtitleClient Subscene = new SubsceneSubtitleClient(); + // movie dbs + public static final TMDbClient TMDb = new TMDbClient(getApplicationProperty("themoviedb.apikey")); + public static EpisodeListProvider[] getEpisodeListProviders() { return new EpisodeListProvider[] { TVRage, AniDB, IMDb, TheTVDB, Serienjunkies }; } + public static MovieIdentificationService[] getMovieIdentificationServices() { + return new MovieIdentificationService[] { OpenSubtitles, TMDb }; + } + + public static SubtitleProvider[] getSubtitleProviders() { return new SubtitleProvider[] { OpenSubtitles, Subscene, Sublight }; } @@ -50,6 +60,26 @@ public final class WebServices { } + public static EpisodeListProvider getEpisodeListProvider(String name) { + for (EpisodeListProvider it : WebServices.getEpisodeListProviders()) { + if (it.getName().equalsIgnoreCase(name)) + return it; + } + + return null; // default + } + + + public static MovieIdentificationService getMovieIdentificationService(String name) { + for (MovieIdentificationService it : getMovieIdentificationServices()) { + if (it.getName().equalsIgnoreCase(name)) + return it; + } + + return null; // default + } + + /** * Dummy constructor to prevent instantiation. */ diff --git a/source/net/sourceforge/filebot/cli/ArgumentBean.java b/source/net/sourceforge/filebot/cli/ArgumentBean.java index bc0393b6..dbf8a835 100644 --- a/source/net/sourceforge/filebot/cli/ArgumentBean.java +++ b/source/net/sourceforge/filebot/cli/ArgumentBean.java @@ -9,6 +9,7 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.logging.Level; import javax.script.ScriptException; @@ -16,40 +17,46 @@ import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.Option; import net.sourceforge.filebot.MediaTypes; -import net.sourceforge.filebot.WebServices; import net.sourceforge.filebot.format.ExpressionFormat; import net.sourceforge.filebot.ui.Language; -import net.sourceforge.filebot.web.EpisodeListProvider; -import net.sourceforge.filebot.web.MovieIdentificationService; public class ArgumentBean { - @Option(name = "-rename-series", usage = "Rename episodes", metaVar = "folder") - public boolean renameSeries; + @Option(name = "-rename", usage = "Rename episode/movie files", metaVar = "fileset") + public boolean rename = false; - @Option(name = "-rename-movie", usage = "Rename movie", metaVar = "folder") - public boolean renameMovie; - - @Option(name = "-get-subtitles", usage = "Fetch subtitles", metaVar = "folder") - public boolean getSubtitles; + @Option(name = "--db", usage = "Episode/Movie database", metaVar = "[TVRage, AniDB, TheTVDB] or [OpenSubtitles, TheMovieDB]") + public String db = null; @Option(name = "--format", usage = "Episode naming scheme", metaVar = "expression") public String format = "{n} - {s+'x'}{e.pad(2)} - {t}"; - @Option(name = "--q", usage = "Search query", metaVar = "name") + @Option(name = "-non-strict", usage = "Use less strict matching") + public boolean nonStrict = false; + + @Option(name = "-get-subtitles", usage = "Fetch subtitles", metaVar = "fileset") + public boolean getSubtitles; + + @Option(name = "--q", usage = "Search query", metaVar = "title") public String query = null; - @Option(name = "--db", usage = "Episode database") - public String db = null; - - @Option(name = "--lang", usage = "Language", metaVar = "language code") + @Option(name = "--lang", usage = "Language", metaVar = "2-letter language code") public String lang = "en"; + @Option(name = "-check", usage = "Create/Check verification file", metaVar = "fileset") + public boolean check; + + @Option(name = "--output", usage = "Output options", metaVar = "[sfv, md5, sha1]") + public String output = "sfv"; + + @Option(name = "--log", usage = "Log level", metaVar = "[all, config, info, warning]") + public String log = "all"; + @Option(name = "-help", usage = "Print this help message") public boolean help = false; - @Option(name = "-open", usage = "Open file", metaVar = "") + @Option(name = "-open", usage = "Open file in GUI", metaVar = "file") public boolean open = false; @Option(name = "-clear", usage = "Clear application settings") @@ -60,12 +67,7 @@ public class ArgumentBean { public boolean runCLI() { - return getSubtitles || renameSeries || renameMovie; - } - - - public boolean printHelp() { - return help; + return getSubtitles || rename || check; } @@ -74,27 +76,13 @@ public class ArgumentBean { } - public boolean clearUserData() { - return clear; + public boolean printHelp() { + return help; } - public List getFiles(boolean resolveFolders) { - List files = new ArrayList(); - - // resolve given paths - for (String argument : arguments) { - try { - File file = new File(argument).getCanonicalFile(); - - // resolve folders - files.addAll(resolveFolders && file.isDirectory() ? listFiles(singleton(file), 0) : singleton(file)); - } catch (IOException e) { - throw new IllegalArgumentException(e); - } - } - - return files; + public boolean clearUserData() { + return clear; } @@ -113,19 +101,27 @@ public class ArgumentBean { } - public EpisodeListProvider getEpisodeListProvider() throws Exception { - if (db == null) - return WebServices.TVRage; - - return (EpisodeListProvider) WebServices.class.getField(db).get(null); + public Level getLogLevel() { + return Level.parse(log.toUpperCase()); } - public MovieIdentificationService getMovieIdentificationService() throws Exception { - if (db == null) - return WebServices.OpenSubtitles; + public List getFiles(boolean resolveFolders) { + List files = new ArrayList(); - return (MovieIdentificationService) WebServices.class.getField(db).get(null); + // resolve given paths + for (String argument : arguments) { + try { + File file = new File(argument).getCanonicalFile(); + + // resolve folders + files.addAll(resolveFolders && file.isDirectory() ? listFiles(singleton(file), 0, false) : singleton(file)); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + } + + return files; } } diff --git a/source/net/sourceforge/filebot/cli/ArgumentProcessor.java b/source/net/sourceforge/filebot/cli/ArgumentProcessor.java index 396629d9..d1b6d456 100644 --- a/source/net/sourceforge/filebot/cli/ArgumentProcessor.java +++ b/source/net/sourceforge/filebot/cli/ArgumentProcessor.java @@ -4,7 +4,9 @@ package net.sourceforge.filebot.cli; import static java.lang.String.*; import static net.sourceforge.filebot.MediaTypes.*; +import static net.sourceforge.filebot.WebServices.*; import static net.sourceforge.filebot.cli.CLILogging.*; +import static net.sourceforge.filebot.hash.VerificationUtilities.*; import static net.sourceforge.tuned.FileUtilities.*; import java.io.File; @@ -13,19 +15,23 @@ import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.ListIterator; import java.util.Locale; import java.util.Map; import java.util.Set; -import java.util.SortedSet; import java.util.TreeSet; import java.util.AbstractMap.SimpleImmutableEntry; import java.util.Map.Entry; +import net.sourceforge.filebot.MediaTypes; import net.sourceforge.filebot.WebServices; import net.sourceforge.filebot.format.EpisodeBindingBean; import net.sourceforge.filebot.format.ExpressionFormat; +import net.sourceforge.filebot.hash.HashType; +import net.sourceforge.filebot.hash.VerificationFileReader; +import net.sourceforge.filebot.hash.VerificationFileWriter; import net.sourceforge.filebot.similarity.Match; import net.sourceforge.filebot.similarity.Matcher; import net.sourceforge.filebot.similarity.NameSimilarityMetric; @@ -50,38 +56,85 @@ public class ArgumentProcessor { public int process(ArgumentBean args) throws Exception { try { - SortedSet files = new TreeSet(args.getFiles(true)); + CLILogger.setLevel(args.getLogLevel()); + Set files = new LinkedHashSet(args.getFiles(true)); if (args.getSubtitles) { List subtitles = getSubtitles(files, args.query, args.getLanguage()); files.addAll(subtitles); } - if (args.renameSeries) { - renameSeries(files, args.query, args.getEpisodeFormat(), args.getEpisodeListProvider(), args.getLanguage().toLocale()); + if (args.rename) { + rename(files, args.query, args.getEpisodeFormat(), args.db, args.getLanguage().toLocale(), !args.nonStrict); } - if (args.renameMovie) { - renameMovie(files, args.getMovieIdentificationService(), args.getLanguage().toLocale()); + if (args.check) { + check(files, args.output); } - CLILogger.fine("Done ヾ(@⌒ー⌒@)ノ"); + CLILogger.finest("Done ヾ(@⌒ー⌒@)ノ"); return 0; } catch (Exception e) { CLILogger.severe(e.getMessage()); - CLILogger.fine("Failure (°_°)"); + CLILogger.finest("Failure (°_°)"); return -1; } } - public void renameSeries(Collection files, String query, ExpressionFormat format, EpisodeListProvider datasource, Locale locale) throws Exception { - List mediaFiles = filter(files, VIDEO_FILES, SUBTITLE_FILES); + public Set rename(Collection files, String query, ExpressionFormat format, String db, Locale locale, boolean strict) throws Exception { + List videoFiles = filter(files, VIDEO_FILES); - if (mediaFiles.isEmpty()) { - throw new IllegalArgumentException("No video or subtitle files: " + files); + if (videoFiles.isEmpty()) { + throw new IllegalArgumentException("No video files: " + files); } + if (getEpisodeListProvider(db) != null) { + // tv series mode + return renameSeries(files, query, format, getEpisodeListProvider(db), locale, strict); + } + + if (getMovieIdentificationService(db) != null) { + // movie mode + return renameMovie(files, getMovieIdentificationService(db), locale); + } + + // auto-determine mode + int sxe = 0; // SxE + int cws = 0; // common word sequence + double max = videoFiles.size(); + + SeriesNameMatcher matcher = new SeriesNameMatcher(); + String[] cwsList = (max >= 5) ? matcher.matchAll(videoFiles.toArray(new File[0])).toArray(new String[0]) : new String[0]; + + for (File f : videoFiles) { + // count SxE matches + if (matcher.matchBySeasonEpisodePattern(f.getName()) != null) { + sxe++; + } + + // count CWS matches + for (String base : cwsList) { + if (base.equalsIgnoreCase(matcher.matchByFirstCommonWordSequence(base, f.getName()))) { + cws++; + break; + } + } + } + + CLILogger.finest(format(Locale.ROOT, "Filename pattern: [%.02f] SxE, [%.02f] CWS", sxe / max, cws / max)); + if (sxe >= (max * 0.65) || cws >= (max * 0.65)) { + return renameSeries(files, query, format, getEpisodeListProviders()[0], locale, strict); // use default episode db + } else { + return renameMovie(files, getMovieIdentificationServices()[0], locale); // use default movie db + } + } + + + public Set renameSeries(Collection files, String query, ExpressionFormat format, EpisodeListProvider db, Locale locale, boolean strict) throws Exception { + CLILogger.config(format("Rename episodes using [%s]", db.getName())); + List mediaFiles = filter(files, VIDEO_FILES, SUBTITLE_FILES); + // auto-detect series name if not given if (query == null) { Collection possibleNames = new SeriesNameMatcher().matchAll(mediaFiles.toArray(new File[0])); @@ -94,17 +147,17 @@ public class ArgumentProcessor { } } - CLILogger.fine(format("Fetching episode data for [%s] from [%s]", query, datasource.getName())); + CLILogger.fine(format("Fetching episode data for [%s]", query)); // find series on the web - SearchResult hit = selectSearchResult(query, datasource.search(query, locale)); + SearchResult hit = selectSearchResult(query, db.search(query, locale)); // fetch episode list - List episodes = datasource.getEpisodeList(hit, locale); + List episodes = db.getEpisodeList(hit, locale); List> matches = new ArrayList>(); - matches.addAll(match(filter(mediaFiles, VIDEO_FILES), episodes)); - matches.addAll(match(filter(mediaFiles, SUBTITLE_FILES), episodes)); + matches.addAll(match(filter(mediaFiles, VIDEO_FILES), episodes, strict)); + matches.addAll(match(filter(mediaFiles, SUBTITLE_FILES), episodes, strict)); if (matches.isEmpty()) { throw new RuntimeException("Unable to match files to episode data"); @@ -126,21 +179,18 @@ public class ArgumentProcessor { } // rename episodes - renameAll(renameMap); + return renameAll(renameMap); } - public void renameMovie(Collection files, MovieIdentificationService datasource, Locale locale) throws Exception { - File[] movieFiles = filter(files, VIDEO_FILES).toArray(new File[0]); + public Set renameMovie(Collection mediaFiles, MovieIdentificationService db, Locale locale) throws Exception { + CLILogger.config(format("Rename movies using [%s]", db.getName())); - if (movieFiles.length <= 0) { - throw new IllegalArgumentException("No video files: " + files); - } - - CLILogger.fine(format("Looking up movie by filehash via [%s]", datasource.getName())); + File[] movieFiles = filter(mediaFiles, VIDEO_FILES).toArray(new File[0]); + CLILogger.fine(format("Looking up movie by filehash via [%s]", db.getName())); // match movie hashes online - MovieDescriptor[] movieByFileHash = datasource.getMovieDescriptors(movieFiles, locale); + MovieDescriptor[] movieByFileHash = db.getMovieDescriptors(movieFiles, locale); // map old files to new paths by applying formatting and validating filenames Map renameMap = new LinkedHashMap(); @@ -161,7 +211,7 @@ public class ArgumentProcessor { } // handle subtitle files - for (File subtitleFile : filter(files, SUBTITLE_FILES)) { + for (File subtitleFile : filter(mediaFiles, SUBTITLE_FILES)) { // check if subtitle corresponds to a movie file (same name, different extension) for (int i = 0; i < movieByFileHash.length; i++) { if (movieByFileHash != null) { @@ -180,7 +230,7 @@ public class ArgumentProcessor { } // rename episodes - renameAll(renameMap); + return renameAll(renameMap); } @@ -278,15 +328,15 @@ public class ArgumentProcessor { } - private void renameAll(Map renameMap) { + private Set renameAll(Map renameMap) throws Exception { // rename files - List> renameLog = new ArrayList>(); + final List> renameLog = new ArrayList>(); try { for (Entry it : renameMap.entrySet()) { try { // rename file, throw exception on failure - File destination = rename(it.getKey(), it.getValue()); + File destination = renameFile(it.getKey(), it.getValue()); CLILogger.info(format("Renamed [%s] to [%s]", it.getKey(), it.getValue())); // remember successfully renamed matches for history entry and possible revert @@ -313,30 +363,46 @@ public class ArgumentProcessor { CLILogger.severe("Failed to revert filename: " + mapping.getValue()); } } + + throw new Exception("Renaming failed", e); + } finally { + if (renameLog.size() > 0) { + // update rename history + HistorySpooler.getInstance().append(renameMap.entrySet()); + + // printer number of renamed files if any + CLILogger.fine(format("Renamed %d files", renameLog.size())); + } } - if (renameLog.size() > 0) { - // update rename history - HistorySpooler.getInstance().append(renameMap.entrySet()); - - // printer number of renamed files if any - CLILogger.fine(format("Renamed %d files", renameLog.size())); - } + // new file names + Set newFiles = new LinkedHashSet(); + for (Entry it : renameLog) + newFiles.add(it.getValue()); + + return newFiles; } - private List> match(List files, List episodes) throws Exception { - // strict SxE metric, don't allow in-between values - SimilarityMetric metric = new SimilarityMetric() { - - @Override - public float getSimilarity(Object o1, Object o2) { - return MatchSimilarityMetric.EpisodeIdentifier.getSimilarity(o1, o2) >= 1 ? 1 : 0; - } - }; + private List> match(List files, List episodes, boolean strict) throws Exception { + SimilarityMetric[] sequence = MatchSimilarityMetric.defaultSequence(); - // fail-fast matcher - Matcher matcher = new Matcher(files, episodes, true, new SimilarityMetric[] { metric }); + 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 + Matcher matcher = new Matcher(files, episodes, true, sequence); List> matches = matcher.match(); for (File failedMatch : matcher.remainingValues()) { @@ -368,4 +434,116 @@ public class ArgumentProcessor { return probableMatches.get(0); } + + public void check(Collection files, String output) throws Exception { + // check verification file + if (containsOnly(files, MediaTypes.getDefaultFilter("verification"))) { + // only check existing hashes + boolean ok = true; + + for (File it : files) { + ok &= check(it, it.getParentFile()); + } + + if (!ok) { + throw new Exception("Data corruption detected"); // one or more hashes mismatch + } + + // all hashes match + return; + } + + // check common parent for all given files + File root = null; + for (File it : files) { + if (root == null || root.getPath().startsWith(it.getParent())) + root = it.getParentFile(); + + if (!it.getParent().startsWith(root.getPath())) + throw new IllegalArgumentException("Path don't share a common root: " + files); + } + + // 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().extensions()[0]); + } + + CLILogger.config("Using output file: " + outputFile); + if (hashType == null) { + throw new IllegalArgumentException("Illegal output type: " + output); + } + + compute(root.getPath(), files, outputFile, hashType); + } + + + public boolean check(File verificationFile, File parent) throws Exception { + HashType type = getHashType(verificationFile); + + // check if type is supported + if (type == null) + throw new IllegalArgumentException("Unsupported format: " + verificationFile); + + // add all file names from verification file + CLILogger.fine(format("Checking [%s]", verificationFile.getName())); + VerificationFileReader parser = new VerificationFileReader(createTextReader(verificationFile), type.getFormat()); + boolean status = true; + + try { + while (parser.hasNext()) { + try { + Entry it = parser.next(); + + File file = new File(parent, it.getKey().getPath()).getAbsoluteFile(); + String current = computeHash(new File(parent, it.getKey().getPath()), type); + CLILogger.info(format("%s %s", current, file)); + + if (current.compareToIgnoreCase(it.getValue()) != 0) { + throw new IOException(format("Corrupted file found: %s [hash mismatch: %s vs %s]", it.getKey(), current, it.getValue())); + } + } catch (IOException e) { + status = false; + CLILogger.warning(e.getMessage()); + } + } + } finally { + parser.close(); + } + + return status; + } + + + private void compute(String root, Collection files, File outputFile, HashType hashType) throws IOException, Exception { + // compute hashes recursively and write to file + VerificationFileWriter out = new VerificationFileWriter(outputFile, hashType.getFormat(), "UTF-8"); + + try { + CLILogger.fine("Computing hashes"); + for (File it : files) { + if (it.isHidden() || MediaTypes.getDefaultFilter("verification").accept(it)) + continue; + + String relativePath = normalizePathSeparators(it.getPath().replace(root, "")).substring(1); + String hash = computeHash(it, hashType); + CLILogger.info(format("%s %s", hash, relativePath)); + + out.write(relativePath, hash); + } + } catch (Exception e) { + outputFile.deleteOnExit(); // delete only partially written files + throw e; + } finally { + out.close(); + } + } } diff --git a/source/net/sourceforge/filebot/cli/CLILogging.java b/source/net/sourceforge/filebot/cli/CLILogging.java index d803b1f8..663ef91e 100644 --- a/source/net/sourceforge/filebot/cli/CLILogging.java +++ b/source/net/sourceforge/filebot/cli/CLILogging.java @@ -31,6 +31,9 @@ public class CLILogging extends Handler { @Override public void publish(LogRecord record) { + if (record.getLevel().intValue() <= getLevel().intValue()) + return; + // print messages to stdout out.println(record.getMessage()); if (record.getThrown() != null) { diff --git a/source/net/sourceforge/filebot/hash/VerificationFileWriter.java b/source/net/sourceforge/filebot/hash/VerificationFileWriter.java new file mode 100644 index 00000000..7587e901 --- /dev/null +++ b/source/net/sourceforge/filebot/hash/VerificationFileWriter.java @@ -0,0 +1,51 @@ + +package net.sourceforge.filebot.hash; + + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Date; + +import net.sourceforge.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(PrintWriter out, VerificationFormat format, String charset) { + this.out = out; + this.format = format; + + // start by printing the file header + writeHeader(charset); + } + + + protected void writeHeader(String charset) { + out.format("; Generated by %s %s on %tF at % files) { - model.addAll(FastFile.foreach(flatten(files, 5))); + model.addAll(FastFile.foreach(flatten(files, 5, false))); } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/NamesListTransferablePolicy.java b/source/net/sourceforge/filebot/ui/panel/rename/NamesListTransferablePolicy.java index 09f29c94..390b5ac3 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/NamesListTransferablePolicy.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/NamesListTransferablePolicy.java @@ -108,7 +108,7 @@ class NamesListTransferablePolicy extends FileTransferablePolicy { loadTorrentFiles(files, values); } else { // load all files from the given folders recursively up do a depth of 5 - values.addAll(FastFile.foreach(flatten(files, 5))); + values.addAll(FastFile.foreach(flatten(files, 5, false))); } model.addAll(values); diff --git a/source/net/sourceforge/filebot/ui/panel/rename/RenameAction.java b/source/net/sourceforge/filebot/ui/panel/rename/RenameAction.java index b8d85195..5440c9a2 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/RenameAction.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/RenameAction.java @@ -42,7 +42,7 @@ class RenameAction extends AbstractAction { try { for (Entry mapping : validate(model.getRenameMap(), getWindow(evt.getSource()))) { // rename file, throw exception on failure - rename(mapping.getKey(), mapping.getValue()); + renameFile(mapping.getKey(), mapping.getValue()); // remember successfully renamed matches for history entry and possible revert renameLog.add(mapping); diff --git a/source/net/sourceforge/filebot/ui/panel/rename/RenamePanel.java b/source/net/sourceforge/filebot/ui/panel/rename/RenamePanel.java index a4e6c952..ba1559f5 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/RenamePanel.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/RenamePanel.java @@ -4,7 +4,6 @@ package net.sourceforge.filebot.ui.panel.rename; import static javax.swing.JOptionPane.*; import static javax.swing.SwingUtilities.*; -import static net.sourceforge.filebot.Settings.*; import static net.sourceforge.filebot.ui.NotificationLogging.*; import static net.sourceforge.tuned.ui.LoadingOverlayPane.*; import static net.sourceforge.tuned.ui.TunedUtilities.*; @@ -47,8 +46,7 @@ import net.sourceforge.filebot.ui.panel.rename.RenameModel.FormattedFuture; import net.sourceforge.filebot.web.Episode; import net.sourceforge.filebot.web.EpisodeListProvider; import net.sourceforge.filebot.web.MovieDescriptor; -import net.sourceforge.filebot.web.OpenSubtitlesClient; -import net.sourceforge.filebot.web.TMDbClient; +import net.sourceforge.filebot.web.MovieIdentificationService; import net.sourceforge.tuned.ExceptionUtilities; import net.sourceforge.tuned.PreferencesMap.PreferencesEntry; import net.sourceforge.tuned.ui.ActionPopup; @@ -157,11 +155,9 @@ public class RenamePanel extends JComponent { actionPopup.addDescription(new JLabel("Movie Mode:")); // create action for movie name completion - OpenSubtitlesClient osdb = new OpenSubtitlesClient(String.format("%s %s", getApplicationName(), getApplicationVersion())); - actionPopup.add(new AutoCompleteAction(osdb.getName(), osdb.getIcon(), new MovieHashMatcher(osdb))); - - TMDbClient tmdb = new TMDbClient(getApplicationProperty("themoviedb.apikey")); - actionPopup.add(new AutoCompleteAction(tmdb.getName(), tmdb.getIcon(), new MovieHashMatcher(tmdb))); + for (MovieIdentificationService it : WebServices.getMovieIdentificationServices()) { + actionPopup.add(new AutoCompleteAction(it.getName(), it.getIcon(), new MovieHashMatcher(it))); + } actionPopup.addSeparator(); actionPopup.addDescription(new JLabel("Options:")); diff --git a/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumTableExportHandler.java b/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumTableExportHandler.java index 210f1a15..7ac4a2fc 100644 --- a/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumTableExportHandler.java +++ b/source/net/sourceforge/filebot/ui/panel/sfv/ChecksumTableExportHandler.java @@ -5,11 +5,9 @@ package net.sourceforge.filebot.ui.panel.sfv; import java.io.File; import java.io.IOException; import java.io.PrintWriter; -import java.util.Date; -import net.sourceforge.filebot.Settings; import net.sourceforge.filebot.hash.HashType; -import net.sourceforge.filebot.hash.VerificationFormat; +import net.sourceforge.filebot.hash.VerificationFileWriter; import net.sourceforge.filebot.ui.transfer.TextFileExportHandler; import net.sourceforge.tuned.FileUtilities; @@ -32,7 +30,7 @@ class ChecksumTableExportHandler extends TextFileExportHandler { @Override public void export(PrintWriter out) { - export(out, defaultColumn()); + export(new VerificationFileWriter(out, model.getHashType().getFormat(), "UTF-8"), defaultColumn(), model.getHashType()); } @@ -54,35 +52,25 @@ class ChecksumTableExportHandler extends TextFileExportHandler { public void export(File file, File column) throws IOException { - PrintWriter out = new PrintWriter(file, "UTF-8"); + VerificationFileWriter writer = new VerificationFileWriter(file, model.getHashType().getFormat(), "UTF-8"); try { - export(out, column); + export(writer, column, model.getHashType()); } finally { - out.close(); + writer.close(); } } - public void export(PrintWriter out, File column) { - HashType hashType = model.getHashType(); - - // print header - out.format("; Generated by %s %s on %tF at % entry = parser.next(); - String name = normalizePath(entry.getKey()); + String name = normalizePathSeparators(entry.getKey().getPath()); String hash = new String(entry.getValue()); ChecksumCell correct = new ChecksumCell(name, file, singletonMap(type, hash)); @@ -138,26 +138,30 @@ class ChecksumTableTransferablePolicy extends BackgroundFileTransferablePolicy