* 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:
parent
fdeb7745e2
commit
01ec6309cc
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# application settings
|
||||
application.name: FileBot
|
||||
application.version: 1.98
|
||||
application.version: 1.99
|
||||
|
||||
thetvdb.apikey: 58B4AA94C59AD656
|
||||
themoviedb.apikey: 5a6edae568130bf10617b6d45be99f13
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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 = "<file>")
|
||||
@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,30 +76,16 @@ public class ArgumentBean {
|
|||
}
|
||||
|
||||
|
||||
public boolean printHelp() {
|
||||
return help;
|
||||
}
|
||||
|
||||
|
||||
public boolean clearUserData() {
|
||||
return clear;
|
||||
}
|
||||
|
||||
|
||||
public List<File> getFiles(boolean resolveFolders) {
|
||||
List<File> files = new ArrayList<File>();
|
||||
|
||||
// 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 ExpressionFormat getEpisodeFormat() throws ScriptException {
|
||||
return new ExpressionFormat(format);
|
||||
}
|
||||
|
@ -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<File> getFiles(boolean resolveFolders) {
|
||||
List<File> files = new ArrayList<File>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<File> files = new TreeSet<File>(args.getFiles(true));
|
||||
CLILogger.setLevel(args.getLogLevel());
|
||||
Set<File> files = new LinkedHashSet<File>(args.getFiles(true));
|
||||
|
||||
if (args.getSubtitles) {
|
||||
List<File> 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<File> files, String query, ExpressionFormat format, EpisodeListProvider datasource, Locale locale) throws Exception {
|
||||
List<File> mediaFiles = filter(files, VIDEO_FILES, SUBTITLE_FILES);
|
||||
public Set<File> rename(Collection<File> files, String query, ExpressionFormat format, String db, Locale locale, boolean strict) throws Exception {
|
||||
List<File> 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<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
|
||||
if (query == null) {
|
||||
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
|
||||
SearchResult hit = selectSearchResult(query, datasource.search(query, locale));
|
||||
SearchResult hit = selectSearchResult(query, db.search(query, locale));
|
||||
|
||||
// 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>>();
|
||||
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<File> files, MovieIdentificationService datasource, Locale locale) throws Exception {
|
||||
File[] movieFiles = filter(files, VIDEO_FILES).toArray(new File[0]);
|
||||
public Set<File> renameMovie(Collection<File> 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<File, String> renameMap = new LinkedHashMap<File, String>();
|
||||
|
@ -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<File, String> renameMap) {
|
||||
private Set<File> renameAll(Map<File, String> renameMap) throws Exception {
|
||||
// rename files
|
||||
List<Entry<File, File>> renameLog = new ArrayList<Entry<File, File>>();
|
||||
final List<Entry<File, File>> renameLog = new ArrayList<Entry<File, File>>();
|
||||
|
||||
try {
|
||||
for (Entry<File, String> 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,8 +363,9 @@ 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());
|
||||
|
@ -324,10 +375,21 @@ public class ArgumentProcessor {
|
|||
}
|
||||
}
|
||||
|
||||
// new file names
|
||||
Set<File> newFiles = new LinkedHashSet<File>();
|
||||
for (Entry<File, File> it : renameLog)
|
||||
newFiles.add(it.getValue());
|
||||
|
||||
private List<Match<File, Episode>> match(List<File> files, List<Episode> episodes) throws Exception {
|
||||
return newFiles;
|
||||
}
|
||||
|
||||
|
||||
private List<Match<File, Episode>> match(List<File> files, List<Episode> episodes, boolean strict) throws Exception {
|
||||
SimilarityMetric[] sequence = MatchSimilarityMetric.defaultSequence();
|
||||
|
||||
if (strict) {
|
||||
// strict SxE metric, don't allow in-between values
|
||||
SimilarityMetric metric = new SimilarityMetric() {
|
||||
SimilarityMetric strictEpisodeMetric = new SimilarityMetric() {
|
||||
|
||||
@Override
|
||||
public float getSimilarity(Object o1, Object o2) {
|
||||
|
@ -335,8 +397,12 @@ public class ArgumentProcessor {
|
|||
}
|
||||
};
|
||||
|
||||
// fail-fast matcher
|
||||
Matcher<File, Episode> matcher = new Matcher<File, Episode>(files, episodes, true, new SimilarityMetric[] { metric });
|
||||
// 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();
|
||||
|
||||
for (File failedMatch : matcher.remainingValues()) {
|
||||
|
@ -368,4 +434,116 @@ public class ArgumentProcessor {
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
Hash hash = type.newHash();
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ class FileListTransferablePolicy extends FileTransferablePolicy {
|
|||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ class FilesListTransferablePolicy extends FileTransferablePolicy {
|
|||
|
||||
@Override
|
||||
protected void load(List<File> files) {
|
||||
model.addAll(FastFile.foreach(flatten(files, 5)));
|
||||
model.addAll(FastFile.foreach(flatten(files, 5, false)));
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -42,7 +42,7 @@ class RenameAction extends AbstractAction {
|
|||
try {
|
||||
for (Entry<File, String> 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);
|
||||
|
|
|
@ -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:"));
|
||||
|
|
|
@ -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 %<tT%n", Settings.getApplicationName(), Settings.getApplicationVersion(), new Date());
|
||||
out.format("; charset=UTF-8%n");
|
||||
out.format(";%n");
|
||||
|
||||
// print data
|
||||
VerificationFormat format = hashType.getFormat();
|
||||
|
||||
public void export(VerificationFileWriter out, File column, HashType type) {
|
||||
for (ChecksumRow row : model.rows()) {
|
||||
ChecksumCell cell = row.getChecksum(column);
|
||||
|
||||
if (cell != null) {
|
||||
String hash = cell.getChecksum(hashType);
|
||||
String hash = cell.getChecksum(type);
|
||||
|
||||
if (hash != null) {
|
||||
out.println(format.format(cell.getName(), hash));
|
||||
out.write(cell.getName(), hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -124,7 +124,7 @@ class ChecksumTableTransferablePolicy extends BackgroundFileTransferablePolicy<C
|
|||
|
||||
Entry<File, String> 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<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())
|
||||
throw new InterruptedException();
|
||||
|
||||
// add next name to relative path
|
||||
relativeFile = new File(relativeFile, file.getName());
|
||||
// ignore hidden files/folders
|
||||
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
|
||||
for (File child : file.listFiles()) {
|
||||
for (File child : absoluteFile.listFiles()) {
|
||||
load(child, relativeFile, root);
|
||||
}
|
||||
} else {
|
||||
String name = normalizePath(relativeFile);
|
||||
String name = normalizePathSeparators(relativeFile.getPath());
|
||||
|
||||
// publish computation cell first
|
||||
publish(createComputationCell(name, root, model.getHashType()));
|
||||
|
||||
// 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()) {
|
||||
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
|
||||
public String getFileFilterDescription() {
|
||||
return "files, folders and sfv files";
|
||||
|
|
|
@ -132,7 +132,7 @@ abstract class SubtitleDropTarget extends JButton {
|
|||
|
||||
if (containsOnly(files, 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) {
|
||||
return handleDownload(videoFiles);
|
||||
|
@ -166,7 +166,7 @@ abstract class SubtitleDropTarget extends JButton {
|
|||
|
||||
private DropAction getDropAction(List<File> 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;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,12 +6,17 @@ import java.io.File;
|
|||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.swing.Icon;
|
||||
|
||||
|
||||
public interface MovieIdentificationService {
|
||||
|
||||
public String getName();
|
||||
|
||||
|
||||
public Icon getIcon();
|
||||
|
||||
|
||||
public List<MovieDescriptor> searchMovie(String query, Locale locale) throws Exception;
|
||||
|
||||
|
||||
|
|
|
@ -38,11 +38,13 @@ public class TMDbClient implements MovieIdentificationService {
|
|||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "TheMovieDB";
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Icon getIcon() {
|
||||
return ResourceManager.getIcon("search.themoviedb");
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ import com.ibm.icu.text.CharsetMatch;
|
|||
|
||||
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);
|
||||
|
||||
// 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>();
|
||||
|
||||
// unfold/flatten file tree
|
||||
for (File root : roots) {
|
||||
if (root.isDirectory()) {
|
||||
listFiles(root, 0, files, maxDepth);
|
||||
listFiles(root, 0, files, maxDepth, listHiddenFiles);
|
||||
} else {
|
||||
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>();
|
||||
|
||||
// collect files from directory tree
|
||||
for (File folder : folders) {
|
||||
listFiles(folder, 0, files, maxDepth);
|
||||
listFiles(folder, 0, files, maxDepth, listHiddenFiles);
|
||||
}
|
||||
|
||||
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)
|
||||
return;
|
||||
|
||||
for (File file : folder.listFiles()) {
|
||||
if (!listHiddenFiles && file.isHidden()) // ignore hidden files
|
||||
continue;
|
||||
|
||||
if (file.isDirectory()) {
|
||||
listFiles(file, depth + 1, files, maxDepth);
|
||||
listFiles(file, depth + 1, files, maxDepth, listHiddenFiles);
|
||||
} else {
|
||||
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) {
|
||||
return Pattern.compile("\\s*[\\\\/]+\\s*").matcher(path).replaceAll(" ");
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue