* 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
This commit is contained in:
Reinhard Pointner 2011-09-13 18:16:38 +00:00
parent fdeb7745e2
commit 01ec6309cc
19 changed files with 433 additions and 173 deletions

View File

@ -20,6 +20,7 @@ import javax.swing.JFrame;
import javax.swing.SwingUtilities; import javax.swing.SwingUtilities;
import javax.swing.UIManager; import javax.swing.UIManager;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser; import org.kohsuke.args4j.CmdLineParser;
import net.sf.ehcache.CacheManager; import net.sf.ehcache.CacheManager;
@ -43,7 +44,19 @@ public class Main {
initializeSecurityManager(); initializeSecurityManager();
// parse arguments // 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()) { if (argumentBean.printHelp()) {
new CmdLineParser(argumentBean).printUsage(System.out); 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;
}
} }

View File

@ -1,6 +1,6 @@
# application settings # application settings
application.name: FileBot application.name: FileBot
application.version: 1.98 application.version: 1.99
thetvdb.apikey: 58B4AA94C59AD656 thetvdb.apikey: 58B4AA94C59AD656
themoviedb.apikey: 5a6edae568130bf10617b6d45be99f13 themoviedb.apikey: 5a6edae568130bf10617b6d45be99f13

View File

@ -7,11 +7,13 @@ import static net.sourceforge.filebot.Settings.*;
import net.sourceforge.filebot.web.AnidbClient; import net.sourceforge.filebot.web.AnidbClient;
import net.sourceforge.filebot.web.EpisodeListProvider; import net.sourceforge.filebot.web.EpisodeListProvider;
import net.sourceforge.filebot.web.IMDbClient; import net.sourceforge.filebot.web.IMDbClient;
import net.sourceforge.filebot.web.MovieIdentificationService;
import net.sourceforge.filebot.web.OpenSubtitlesClient; import net.sourceforge.filebot.web.OpenSubtitlesClient;
import net.sourceforge.filebot.web.SerienjunkiesClient; import net.sourceforge.filebot.web.SerienjunkiesClient;
import net.sourceforge.filebot.web.SublightSubtitleClient; import net.sourceforge.filebot.web.SublightSubtitleClient;
import net.sourceforge.filebot.web.SubsceneSubtitleClient; import net.sourceforge.filebot.web.SubsceneSubtitleClient;
import net.sourceforge.filebot.web.SubtitleProvider; import net.sourceforge.filebot.web.SubtitleProvider;
import net.sourceforge.filebot.web.TMDbClient;
import net.sourceforge.filebot.web.TVRageClient; import net.sourceforge.filebot.web.TVRageClient;
import net.sourceforge.filebot.web.TheTVDBClient; import net.sourceforge.filebot.web.TheTVDBClient;
import net.sourceforge.filebot.web.VideoHashSubtitleService; 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 SublightSubtitleClient Sublight = new SublightSubtitleClient(getApplicationName(), getApplicationProperty("sublight.apikey"));
public static final SubsceneSubtitleClient Subscene = new SubsceneSubtitleClient(); public static final SubsceneSubtitleClient Subscene = new SubsceneSubtitleClient();
// movie dbs
public static final TMDbClient TMDb = new TMDbClient(getApplicationProperty("themoviedb.apikey"));
public static EpisodeListProvider[] getEpisodeListProviders() { public static EpisodeListProvider[] getEpisodeListProviders() {
return new EpisodeListProvider[] { TVRage, AniDB, IMDb, TheTVDB, Serienjunkies }; return new EpisodeListProvider[] { TVRage, AniDB, IMDb, TheTVDB, Serienjunkies };
} }
public static MovieIdentificationService[] getMovieIdentificationServices() {
return new MovieIdentificationService[] { OpenSubtitles, TMDb };
}
public static SubtitleProvider[] getSubtitleProviders() { public static SubtitleProvider[] getSubtitleProviders() {
return new SubtitleProvider[] { OpenSubtitles, Subscene, Sublight }; 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. * Dummy constructor to prevent instantiation.
*/ */

View File

@ -9,6 +9,7 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.logging.Level;
import javax.script.ScriptException; import javax.script.ScriptException;
@ -16,40 +17,46 @@ import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option; import org.kohsuke.args4j.Option;
import net.sourceforge.filebot.MediaTypes; import net.sourceforge.filebot.MediaTypes;
import net.sourceforge.filebot.WebServices;
import net.sourceforge.filebot.format.ExpressionFormat; import net.sourceforge.filebot.format.ExpressionFormat;
import net.sourceforge.filebot.ui.Language; import net.sourceforge.filebot.ui.Language;
import net.sourceforge.filebot.web.EpisodeListProvider;
import net.sourceforge.filebot.web.MovieIdentificationService;
public class ArgumentBean { public class ArgumentBean {
@Option(name = "-rename-series", usage = "Rename episodes", metaVar = "folder") @Option(name = "-rename", usage = "Rename episode/movie files", metaVar = "fileset")
public boolean renameSeries; public boolean rename = false;
@Option(name = "-rename-movie", usage = "Rename movie", metaVar = "folder") @Option(name = "--db", usage = "Episode/Movie database", metaVar = "[TVRage, AniDB, TheTVDB] or [OpenSubtitles, TheMovieDB]")
public boolean renameMovie; public String db = null;
@Option(name = "-get-subtitles", usage = "Fetch subtitles", metaVar = "folder")
public boolean getSubtitles;
@Option(name = "--format", usage = "Episode naming scheme", metaVar = "expression") @Option(name = "--format", usage = "Episode naming scheme", metaVar = "expression")
public String format = "{n} - {s+'x'}{e.pad(2)} - {t}"; 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; public String query = null;
@Option(name = "--db", usage = "Episode database") @Option(name = "--lang", usage = "Language", metaVar = "2-letter language code")
public String db = null;
@Option(name = "--lang", usage = "Language", metaVar = "language code")
public String lang = "en"; 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") @Option(name = "-help", usage = "Print this help message")
public boolean help = false; public boolean help = false;
@Option(name = "-open", usage = "Open file", metaVar = "<file>") @Option(name = "-open", usage = "Open file in GUI", metaVar = "file")
public boolean open = false; public boolean open = false;
@Option(name = "-clear", usage = "Clear application settings") @Option(name = "-clear", usage = "Clear application settings")
@ -60,12 +67,7 @@ public class ArgumentBean {
public boolean runCLI() { public boolean runCLI() {
return getSubtitles || renameSeries || renameMovie; return getSubtitles || rename || check;
}
public boolean printHelp() {
return help;
} }
@ -74,27 +76,13 @@ public class ArgumentBean {
} }
public boolean clearUserData() { public boolean printHelp() {
return clear; return help;
} }
public List<File> getFiles(boolean resolveFolders) { public boolean clearUserData() {
List<File> files = new ArrayList<File>(); return clear;
// 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;
} }
@ -113,19 +101,27 @@ public class ArgumentBean {
} }
public EpisodeListProvider getEpisodeListProvider() throws Exception { public Level getLogLevel() {
if (db == null) return Level.parse(log.toUpperCase());
return WebServices.TVRage;
return (EpisodeListProvider) WebServices.class.getField(db).get(null);
} }
public MovieIdentificationService getMovieIdentificationService() throws Exception { public List<File> getFiles(boolean resolveFolders) {
if (db == null) List<File> files = new ArrayList<File>();
return WebServices.OpenSubtitles;
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;
} }
} }

View File

@ -4,7 +4,9 @@ package net.sourceforge.filebot.cli;
import static java.lang.String.*; import static java.lang.String.*;
import static net.sourceforge.filebot.MediaTypes.*; import static net.sourceforge.filebot.MediaTypes.*;
import static net.sourceforge.filebot.WebServices.*;
import static net.sourceforge.filebot.cli.CLILogging.*; import static net.sourceforge.filebot.cli.CLILogging.*;
import static net.sourceforge.filebot.hash.VerificationUtilities.*;
import static net.sourceforge.tuned.FileUtilities.*; import static net.sourceforge.tuned.FileUtilities.*;
import java.io.File; import java.io.File;
@ -13,19 +15,23 @@ import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.ListIterator; import java.util.ListIterator;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.AbstractMap.SimpleImmutableEntry; import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.Map.Entry; import java.util.Map.Entry;
import net.sourceforge.filebot.MediaTypes;
import net.sourceforge.filebot.WebServices; import net.sourceforge.filebot.WebServices;
import net.sourceforge.filebot.format.EpisodeBindingBean; import net.sourceforge.filebot.format.EpisodeBindingBean;
import net.sourceforge.filebot.format.ExpressionFormat; 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.Match;
import net.sourceforge.filebot.similarity.Matcher; import net.sourceforge.filebot.similarity.Matcher;
import net.sourceforge.filebot.similarity.NameSimilarityMetric; import net.sourceforge.filebot.similarity.NameSimilarityMetric;
@ -50,38 +56,85 @@ public class ArgumentProcessor {
public int process(ArgumentBean args) throws Exception { public int process(ArgumentBean args) throws Exception {
try { try {
SortedSet<File> files = new TreeSet<File>(args.getFiles(true)); CLILogger.setLevel(args.getLogLevel());
Set<File> files = new LinkedHashSet<File>(args.getFiles(true));
if (args.getSubtitles) { if (args.getSubtitles) {
List<File> subtitles = getSubtitles(files, args.query, args.getLanguage()); List<File> subtitles = getSubtitles(files, args.query, args.getLanguage());
files.addAll(subtitles); files.addAll(subtitles);
} }
if (args.renameSeries) { if (args.rename) {
renameSeries(files, args.query, args.getEpisodeFormat(), args.getEpisodeListProvider(), args.getLanguage().toLocale()); rename(files, args.query, args.getEpisodeFormat(), args.db, args.getLanguage().toLocale(), !args.nonStrict);
} }
if (args.renameMovie) { if (args.check) {
renameMovie(files, args.getMovieIdentificationService(), args.getLanguage().toLocale()); check(files, args.output);
} }
CLILogger.fine("Done ヾ(@⌒ー⌒@)"); CLILogger.finest("Done ヾ(@⌒ー⌒@)");
return 0; return 0;
} catch (Exception e) { } catch (Exception e) {
CLILogger.severe(e.getMessage()); CLILogger.severe(e.getMessage());
CLILogger.fine("Failure (°_°)"); CLILogger.finest("Failure (°_°)");
return -1; return -1;
} }
} }
public void renameSeries(Collection<File> files, String query, ExpressionFormat format, EpisodeListProvider datasource, Locale locale) throws Exception { public Set<File> rename(Collection<File> files, String query, ExpressionFormat format, String db, Locale locale, boolean strict) throws Exception {
List<File> mediaFiles = filter(files, VIDEO_FILES, SUBTITLE_FILES); List<File> videoFiles = filter(files, VIDEO_FILES);
if (mediaFiles.isEmpty()) { if (videoFiles.isEmpty()) {
throw new IllegalArgumentException("No video or subtitle files: " + files); 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<File> renameSeries(Collection<File> files, String query, ExpressionFormat format, EpisodeListProvider db, Locale locale, boolean strict) throws Exception {
CLILogger.config(format("Rename episodes using [%s]", db.getName()));
List<File> mediaFiles = filter(files, VIDEO_FILES, SUBTITLE_FILES);
// auto-detect series name if not given // auto-detect series name if not given
if (query == null) { if (query == null) {
Collection<String> possibleNames = new SeriesNameMatcher().matchAll(mediaFiles.toArray(new File[0])); Collection<String> 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 // find series on the web
SearchResult hit = selectSearchResult(query, datasource.search(query, locale)); SearchResult hit = selectSearchResult(query, db.search(query, locale));
// fetch episode list // fetch episode list
List<Episode> episodes = datasource.getEpisodeList(hit, locale); List<Episode> episodes = db.getEpisodeList(hit, locale);
List<Match<File, Episode>> matches = new ArrayList<Match<File, Episode>>(); List<Match<File, Episode>> matches = new ArrayList<Match<File, Episode>>();
matches.addAll(match(filter(mediaFiles, VIDEO_FILES), episodes)); matches.addAll(match(filter(mediaFiles, VIDEO_FILES), episodes, strict));
matches.addAll(match(filter(mediaFiles, SUBTITLE_FILES), episodes)); matches.addAll(match(filter(mediaFiles, SUBTITLE_FILES), episodes, strict));
if (matches.isEmpty()) { if (matches.isEmpty()) {
throw new RuntimeException("Unable to match files to episode data"); throw new RuntimeException("Unable to match files to episode data");
@ -126,21 +179,18 @@ public class ArgumentProcessor {
} }
// rename episodes // rename episodes
renameAll(renameMap); return renameAll(renameMap);
} }
public void renameMovie(Collection<File> files, MovieIdentificationService datasource, Locale locale) throws Exception { public Set<File> renameMovie(Collection<File> mediaFiles, MovieIdentificationService db, Locale locale) throws Exception {
File[] movieFiles = filter(files, VIDEO_FILES).toArray(new File[0]); CLILogger.config(format("Rename movies using [%s]", db.getName()));
if (movieFiles.length <= 0) { File[] movieFiles = filter(mediaFiles, VIDEO_FILES).toArray(new File[0]);
throw new IllegalArgumentException("No video files: " + files); CLILogger.fine(format("Looking up movie by filehash via [%s]", db.getName()));
}
CLILogger.fine(format("Looking up movie by filehash via [%s]", datasource.getName()));
// match movie hashes online // 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 old files to new paths by applying formatting and validating filenames
Map<File, String> renameMap = new LinkedHashMap<File, String>(); Map<File, String> renameMap = new LinkedHashMap<File, String>();
@ -161,7 +211,7 @@ public class ArgumentProcessor {
} }
// handle subtitle files // 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) // check if subtitle corresponds to a movie file (same name, different extension)
for (int i = 0; i < movieByFileHash.length; i++) { for (int i = 0; i < movieByFileHash.length; i++) {
if (movieByFileHash != null) { if (movieByFileHash != null) {
@ -180,7 +230,7 @@ public class ArgumentProcessor {
} }
// rename episodes // rename episodes
renameAll(renameMap); return renameAll(renameMap);
} }
@ -278,15 +328,15 @@ public class ArgumentProcessor {
} }
private void renameAll(Map<File, String> renameMap) { private Set<File> renameAll(Map<File, String> renameMap) throws Exception {
// rename files // rename files
List<Entry<File, File>> renameLog = new ArrayList<Entry<File, File>>(); final List<Entry<File, File>> renameLog = new ArrayList<Entry<File, File>>();
try { try {
for (Entry<File, String> it : renameMap.entrySet()) { for (Entry<File, String> it : renameMap.entrySet()) {
try { try {
// rename file, throw exception on failure // 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())); CLILogger.info(format("Renamed [%s] to [%s]", it.getKey(), it.getValue()));
// remember successfully renamed matches for history entry and possible revert // 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()); 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) { // new file names
// update rename history Set<File> newFiles = new LinkedHashSet<File>();
HistorySpooler.getInstance().append(renameMap.entrySet()); for (Entry<File, File> it : renameLog)
newFiles.add(it.getValue());
// printer number of renamed files if any
CLILogger.fine(format("Renamed %d files", renameLog.size())); return newFiles;
}
} }
private List<Match<File, Episode>> match(List<File> files, List<Episode> episodes) throws Exception { private List<Match<File, Episode>> match(List<File> files, List<Episode> episodes, boolean strict) throws Exception {
// strict SxE metric, don't allow in-between values SimilarityMetric[] sequence = MatchSimilarityMetric.defaultSequence();
SimilarityMetric metric = new SimilarityMetric() {
@Override
public float getSimilarity(Object o1, Object o2) {
return MatchSimilarityMetric.EpisodeIdentifier.getSimilarity(o1, o2) >= 1 ? 1 : 0;
}
};
// fail-fast matcher if (strict) {
Matcher<File, Episode> matcher = new Matcher<File, Episode>(files, episodes, true, new SimilarityMetric[] { metric }); // 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<File, Episode> matcher = new Matcher<File, Episode>(files, episodes, true, sequence);
List<Match<File, Episode>> matches = matcher.match(); List<Match<File, Episode>> matches = matcher.match();
for (File failedMatch : matcher.remainingValues()) { for (File failedMatch : matcher.remainingValues()) {
@ -368,4 +434,116 @@ public class ArgumentProcessor {
return probableMatches.get(0); return probableMatches.get(0);
} }
public void check(Collection<File> 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<File, String> 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<File> 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();
}
}
} }

View File

@ -31,6 +31,9 @@ public class CLILogging extends Handler {
@Override @Override
public void publish(LogRecord record) { public void publish(LogRecord record) {
if (record.getLevel().intValue() <= getLevel().intValue())
return;
// print messages to stdout // print messages to stdout
out.println(record.getMessage()); out.println(record.getMessage());
if (record.getThrown() != null) { if (record.getThrown() != null) {

View File

@ -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 %<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

@ -86,6 +86,16 @@ public final class VerificationUtilities {
} }
public static HashType getHashTypeByExtension(String extension) {
for (HashType hashType : HashType.values()) {
if (hashType.getFilter().acceptExtension(extension))
return hashType;
}
return null;
}
public static String computeHash(File file, HashType type) throws IOException, InterruptedException { public static String computeHash(File file, HashType type) throws IOException, InterruptedException {
Hash hash = type.newHash(); Hash hash = type.newHash();

View File

@ -55,7 +55,7 @@ class FileListTransferablePolicy extends FileTransferablePolicy {
} }
// load all files from the given folders recursively up do a depth of 5 // load all files from the given folders recursively up do a depth of 5
for (File file : flatten(files, 5)) { for (File file : flatten(files, 5, false)) {
list.getModel().add(FileUtilities.getName(file)); list.getModel().add(FileUtilities.getName(file));
} }
} }

View File

@ -35,7 +35,7 @@ class FilesListTransferablePolicy extends FileTransferablePolicy {
@Override @Override
protected void load(List<File> files) { protected void load(List<File> files) {
model.addAll(FastFile.foreach(flatten(files, 5))); model.addAll(FastFile.foreach(flatten(files, 5, false)));
} }

View File

@ -108,7 +108,7 @@ class NamesListTransferablePolicy extends FileTransferablePolicy {
loadTorrentFiles(files, values); loadTorrentFiles(files, values);
} else { } else {
// load all files from the given folders recursively up do a depth of 5 // 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); model.addAll(values);

View File

@ -42,7 +42,7 @@ class RenameAction extends AbstractAction {
try { try {
for (Entry<File, String> mapping : validate(model.getRenameMap(), getWindow(evt.getSource()))) { for (Entry<File, String> mapping : validate(model.getRenameMap(), getWindow(evt.getSource()))) {
// rename file, throw exception on failure // 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 // remember successfully renamed matches for history entry and possible revert
renameLog.add(mapping); renameLog.add(mapping);

View File

@ -4,7 +4,6 @@ package net.sourceforge.filebot.ui.panel.rename;
import static javax.swing.JOptionPane.*; import static javax.swing.JOptionPane.*;
import static javax.swing.SwingUtilities.*; import static javax.swing.SwingUtilities.*;
import static net.sourceforge.filebot.Settings.*;
import static net.sourceforge.filebot.ui.NotificationLogging.*; import static net.sourceforge.filebot.ui.NotificationLogging.*;
import static net.sourceforge.tuned.ui.LoadingOverlayPane.*; import static net.sourceforge.tuned.ui.LoadingOverlayPane.*;
import static net.sourceforge.tuned.ui.TunedUtilities.*; 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.Episode;
import net.sourceforge.filebot.web.EpisodeListProvider; import net.sourceforge.filebot.web.EpisodeListProvider;
import net.sourceforge.filebot.web.MovieDescriptor; import net.sourceforge.filebot.web.MovieDescriptor;
import net.sourceforge.filebot.web.OpenSubtitlesClient; import net.sourceforge.filebot.web.MovieIdentificationService;
import net.sourceforge.filebot.web.TMDbClient;
import net.sourceforge.tuned.ExceptionUtilities; import net.sourceforge.tuned.ExceptionUtilities;
import net.sourceforge.tuned.PreferencesMap.PreferencesEntry; import net.sourceforge.tuned.PreferencesMap.PreferencesEntry;
import net.sourceforge.tuned.ui.ActionPopup; import net.sourceforge.tuned.ui.ActionPopup;
@ -157,11 +155,9 @@ public class RenamePanel extends JComponent {
actionPopup.addDescription(new JLabel("Movie Mode:")); actionPopup.addDescription(new JLabel("Movie Mode:"));
// create action for movie name completion // create action for movie name completion
OpenSubtitlesClient osdb = new OpenSubtitlesClient(String.format("%s %s", getApplicationName(), getApplicationVersion())); for (MovieIdentificationService it : WebServices.getMovieIdentificationServices()) {
actionPopup.add(new AutoCompleteAction(osdb.getName(), osdb.getIcon(), new MovieHashMatcher(osdb))); actionPopup.add(new AutoCompleteAction(it.getName(), it.getIcon(), new MovieHashMatcher(it)));
}
TMDbClient tmdb = new TMDbClient(getApplicationProperty("themoviedb.apikey"));
actionPopup.add(new AutoCompleteAction(tmdb.getName(), tmdb.getIcon(), new MovieHashMatcher(tmdb)));
actionPopup.addSeparator(); actionPopup.addSeparator();
actionPopup.addDescription(new JLabel("Options:")); actionPopup.addDescription(new JLabel("Options:"));

View File

@ -5,11 +5,9 @@ package net.sourceforge.filebot.ui.panel.sfv;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.util.Date;
import net.sourceforge.filebot.Settings;
import net.sourceforge.filebot.hash.HashType; 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.filebot.ui.transfer.TextFileExportHandler;
import net.sourceforge.tuned.FileUtilities; import net.sourceforge.tuned.FileUtilities;
@ -32,7 +30,7 @@ class ChecksumTableExportHandler extends TextFileExportHandler {
@Override @Override
public void export(PrintWriter out) { 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 { 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 { try {
export(out, column); export(writer, column, model.getHashType());
} finally { } finally {
out.close(); writer.close();
} }
} }
public void export(PrintWriter out, File column) { public void export(VerificationFileWriter out, File column, HashType type) {
HashType hashType = model.getHashType();
// print header
out.format("; Generated by %s %s on %tF at %<tT%n", Settings.getApplicationName(), Settings.getApplicationVersion(), new Date());
out.format("; charset=UTF-8%n");
out.format(";%n");
// print data
VerificationFormat format = hashType.getFormat();
for (ChecksumRow row : model.rows()) { for (ChecksumRow row : model.rows()) {
ChecksumCell cell = row.getChecksum(column); ChecksumCell cell = row.getChecksum(column);
if (cell != null) { if (cell != null) {
String hash = cell.getChecksum(hashType); String hash = cell.getChecksum(type);
if (hash != null) { if (hash != null) {
out.println(format.format(cell.getName(), hash)); out.write(cell.getName(), hash);
} }
} }
} }

View File

@ -124,7 +124,7 @@ class ChecksumTableTransferablePolicy extends BackgroundFileTransferablePolicy<C
Entry<File, String> entry = parser.next(); Entry<File, String> entry = parser.next();
String name = normalizePath(entry.getKey()); String name = normalizePathSeparators(entry.getKey().getPath());
String hash = new String(entry.getValue()); String hash = new String(entry.getValue());
ChecksumCell correct = new ChecksumCell(name, file, singletonMap(type, hash)); ChecksumCell correct = new ChecksumCell(name, file, singletonMap(type, hash));
@ -138,26 +138,30 @@ class ChecksumTableTransferablePolicy extends BackgroundFileTransferablePolicy<C
} }
protected void load(File file, File relativeFile, File root) throws IOException, InterruptedException { protected void load(File absoluteFile, File relativeFile, File root) throws IOException, InterruptedException {
if (Thread.interrupted()) if (Thread.interrupted())
throw new InterruptedException(); throw new InterruptedException();
// add next name to relative path // ignore hidden files/folders
relativeFile = new File(relativeFile, file.getName()); if (absoluteFile.isHidden())
return;
if (file.isDirectory()) { // add next name to relative path
relativeFile = new File(relativeFile, absoluteFile.getName());
if (absoluteFile.isDirectory()) {
// load all files in the file tree // load all files in the file tree
for (File child : file.listFiles()) { for (File child : absoluteFile.listFiles()) {
load(child, relativeFile, root); load(child, relativeFile, root);
} }
} else { } else {
String name = normalizePath(relativeFile); String name = normalizePathSeparators(relativeFile.getPath());
// publish computation cell first // publish computation cell first
publish(createComputationCell(name, root, model.getHashType())); publish(createComputationCell(name, root, model.getHashType()));
// publish verification cell, if we can // publish verification cell, if we can
Map<File, String> hashByVerificationFile = verificationTracker.get().getHashByVerificationFile(file); Map<File, String> hashByVerificationFile = verificationTracker.get().getHashByVerificationFile(absoluteFile);
for (Entry<File, String> entry : hashByVerificationFile.entrySet()) { for (Entry<File, String> entry : hashByVerificationFile.entrySet()) {
HashType hashType = verificationTracker.get().getVerificationFileType(entry.getKey()); HashType hashType = verificationTracker.get().getVerificationFileType(entry.getKey());
@ -177,11 +181,6 @@ class ChecksumTableTransferablePolicy extends BackgroundFileTransferablePolicy<C
} }
protected String normalizePath(File file) {
return file.getPath().replace('\\', '/');
}
@Override @Override
public String getFileFilterDescription() { public String getFileFilterDescription() {
return "files, folders and sfv files"; return "files, folders and sfv files";

View File

@ -132,7 +132,7 @@ abstract class SubtitleDropTarget extends JButton {
if (containsOnly(files, FOLDERS)) { if (containsOnly(files, FOLDERS)) {
// collect all video files from the dropped folders // collect all video files from the dropped folders
List<File> videoFiles = filter(listFiles(files, 0), VIDEO_FILES); List<File> videoFiles = filter(listFiles(files, 0, false), VIDEO_FILES);
if (videoFiles.size() > 0) { if (videoFiles.size() > 0) {
return handleDownload(videoFiles); return handleDownload(videoFiles);
@ -166,7 +166,7 @@ abstract class SubtitleDropTarget extends JButton {
private DropAction getDropAction(List<File> files) { private DropAction getDropAction(List<File> files) {
// video files only, or any folder, containing video files // video files only, or any folder, containing video files
if (containsOnly(files, VIDEO_FILES) || (containsOnly(files, FOLDERS) && filter(listFiles(files, 0), VIDEO_FILES).size() > 0)) { if (containsOnly(files, VIDEO_FILES) || (containsOnly(files, FOLDERS) && filter(listFiles(files, 0, false), VIDEO_FILES).size() > 0)) {
return DropAction.Download; return DropAction.Download;
} }

View File

@ -6,12 +6,17 @@ import java.io.File;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import javax.swing.Icon;
public interface MovieIdentificationService { public interface MovieIdentificationService {
public String getName(); public String getName();
public Icon getIcon();
public List<MovieDescriptor> searchMovie(String query, Locale locale) throws Exception; public List<MovieDescriptor> searchMovie(String query, Locale locale) throws Exception;

View File

@ -38,11 +38,13 @@ public class TMDbClient implements MovieIdentificationService {
} }
@Override
public String getName() { public String getName() {
return "TheMovieDB"; return "TheMovieDB";
} }
@Override
public Icon getIcon() { public Icon getIcon() {
return ResourceManager.getIcon("search.themoviedb"); return ResourceManager.getIcon("search.themoviedb");
} }

View File

@ -29,7 +29,7 @@ import com.ibm.icu.text.CharsetMatch;
public final class FileUtilities { public final class FileUtilities {
public static File rename(File source, String newPath) throws IOException { public static File renameFile(File source, String newPath) throws IOException {
File destination = new File(newPath); File destination = new File(newPath);
// resolve destination // resolve destination
@ -203,13 +203,13 @@ public final class FileUtilities {
} }
public static List<File> flatten(Iterable<File> roots, int maxDepth) { public static List<File> flatten(Iterable<File> roots, int maxDepth, boolean listHiddenFiles) {
List<File> files = new ArrayList<File>(); List<File> files = new ArrayList<File>();
// unfold/flatten file tree // unfold/flatten file tree
for (File root : roots) { for (File root : roots) {
if (root.isDirectory()) { if (root.isDirectory()) {
listFiles(root, 0, files, maxDepth); listFiles(root, 0, files, maxDepth, listHiddenFiles);
} else { } else {
files.add(root); files.add(root);
} }
@ -230,25 +230,28 @@ public final class FileUtilities {
} }
public static List<File> listFiles(Iterable<File> folders, int maxDepth) { public static List<File> listFiles(Iterable<File> folders, int maxDepth, boolean listHiddenFiles) {
List<File> files = new ArrayList<File>(); List<File> files = new ArrayList<File>();
// collect files from directory tree // collect files from directory tree
for (File folder : folders) { for (File folder : folders) {
listFiles(folder, 0, files, maxDepth); listFiles(folder, 0, files, maxDepth, listHiddenFiles);
} }
return files; return files;
} }
private static void listFiles(File folder, int depth, List<File> files, int maxDepth) { private static void listFiles(File folder, int depth, List<File> files, int maxDepth, boolean listHiddenFiles) {
if (depth > maxDepth) if (depth > maxDepth)
return; return;
for (File file : folder.listFiles()) { for (File file : folder.listFiles()) {
if (!listHiddenFiles && file.isHidden()) // ignore hidden files
continue;
if (file.isDirectory()) { if (file.isDirectory()) {
listFiles(file, depth + 1, files, maxDepth); listFiles(file, depth + 1, files, maxDepth, listHiddenFiles);
} else { } else {
files.add(file); files.add(file);
} }
@ -338,6 +341,11 @@ public final class FileUtilities {
} }
public static String normalizePathSeparators(String path) {
return path.replace('\\', '/');
}
public static String replacePathSeparators(CharSequence path) { public static String replacePathSeparators(CharSequence path) {
return Pattern.compile("\\s*[\\\\/]+\\s*").matcher(path).replaceAll(" "); return Pattern.compile("\\s*[\\\\/]+\\s*").matcher(path).replaceAll(" ");
} }