From aa10510e87b3d6a9f72c393164a9eabf94f3f749 Mon Sep 17 00:00:00 2001 From: Reinhard Pointner Date: Tue, 18 Apr 2017 15:25:34 +0800 Subject: [PATCH] Added `-exec` option that works similar to `find -exec` and the `--def exec` option. e.g. ``` filebot -rename $OPTS -exec echo {f} filebot -rename $OPTS -exec echo {f} + ``` --- source/net/filebot/cli/ArgumentBean.java | 12 +++ source/net/filebot/cli/ArgumentProcessor.java | 4 +- source/net/filebot/cli/CmdlineInterface.java | 4 +- source/net/filebot/cli/CmdlineOperations.java | 95 ++++++++++-------- .../filebot/cli/CmdlineOperationsTextUI.java | 6 +- source/net/filebot/cli/ExecCommand.java | 99 +++++++++++++++++++ .../net/filebot/cli/ScriptShellBaseClass.java | 2 +- 7 files changed, 171 insertions(+), 51 deletions(-) create mode 100644 source/net/filebot/cli/ExecCommand.java diff --git a/source/net/filebot/cli/ArgumentBean.java b/source/net/filebot/cli/ArgumentBean.java index 98e18b78..4ff5ac49 100644 --- a/source/net/filebot/cli/ArgumentBean.java +++ b/source/net/filebot/cli/ArgumentBean.java @@ -27,6 +27,7 @@ import org.kohsuke.args4j.CmdLineParser; import org.kohsuke.args4j.Option; import org.kohsuke.args4j.ParserProperties; import org.kohsuke.args4j.spi.ExplicitBooleanOptionHandler; +import org.kohsuke.args4j.spi.RestOfArgumentsHandler; import net.filebot.ApplicationFolder; import net.filebot.Language; @@ -140,6 +141,9 @@ public class ArgumentBean { @Option(name = "--def", usage = "Define script variables", handler = BindingsHandler.class) public Map defines = new LinkedHashMap(); + @Option(name = "-exec", usage = "Execute command", handler = RestOfArgumentsHandler.class) + public List exec = new ArrayList(); + @Argument public List arguments = new ArrayList(); @@ -307,6 +311,14 @@ public class ArgumentBean { return Level.parse(log.toUpperCase()); } + public ExecCommand getExecCommand() { + try { + return exec == null || exec.isEmpty() ? null : ExecCommand.parse(exec, getOutputPath()); + } catch (Exception e) { + throw new CmdlineException("Illegal exec expression: " + exec); + } + } + public PanelBuilder[] getPanelBuilders() { // default multi panel mode if (mode == null) { diff --git a/source/net/filebot/cli/ArgumentProcessor.java b/source/net/filebot/cli/ArgumentProcessor.java index 2e4f044e..50d6f470 100644 --- a/source/net/filebot/cli/ArgumentProcessor.java +++ b/source/net/filebot/cli/ArgumentProcessor.java @@ -55,7 +55,7 @@ public class ArgumentProcessor { // rename files in linear order if (args.list && args.rename) { - return cli.rename(args.getEpisodeListProvider(), args.getSearchQuery(), args.getExpressionFileFormat(), args.getExpressionFilter(), args.getSortOrder(), args.getLanguage().getLocale(), args.isStrict(), args.getFiles(true), args.getRenameAction(), args.getConflictAction(), args.getOutputPath()).isEmpty() ? 1 : 0; + return cli.rename(args.getEpisodeListProvider(), args.getSearchQuery(), args.getExpressionFileFormat(), args.getExpressionFilter(), args.getSortOrder(), args.getLanguage().getLocale(), args.isStrict(), args.getFiles(true), args.getRenameAction(), args.getConflictAction(), args.getOutputPath(), args.getExecCommand()).isEmpty() ? 1 : 0; } // print episode info @@ -85,7 +85,7 @@ public class ArgumentProcessor { } if (args.rename) { - cli.rename(files, args.getRenameAction(), args.getConflictAction(), args.getAbsoluteOutputFolder(), args.getExpressionFileFormat(), args.getDatasource(), args.getSearchQuery(), args.getSortOrder(), args.getExpressionFilter(), args.getLanguage().getLocale(), args.isStrict()); + cli.rename(files, args.getRenameAction(), args.getConflictAction(), args.getAbsoluteOutputFolder(), args.getExpressionFileFormat(), args.getDatasource(), args.getSearchQuery(), args.getSortOrder(), args.getExpressionFilter(), args.getLanguage().getLocale(), args.isStrict(), args.getExecCommand()); } if (args.check) { diff --git a/source/net/filebot/cli/CmdlineInterface.java b/source/net/filebot/cli/CmdlineInterface.java index ddca2a49..c463bf7b 100644 --- a/source/net/filebot/cli/CmdlineInterface.java +++ b/source/net/filebot/cli/CmdlineInterface.java @@ -23,9 +23,9 @@ import net.filebot.web.SortOrder; public interface CmdlineInterface { - List rename(Collection files, RenameAction action, ConflictAction conflict, File output, ExpressionFileFormat format, Datasource db, String query, SortOrder order, ExpressionFilter filter, Locale locale, boolean strict) throws Exception; + List rename(Collection files, RenameAction action, ConflictAction conflict, File output, ExpressionFileFormat format, Datasource db, String query, SortOrder order, ExpressionFilter filter, Locale locale, boolean strict, ExecCommand exec) throws Exception; - List rename(EpisodeListProvider db, String query, ExpressionFileFormat format, ExpressionFilter filter, SortOrder order, Locale locale, boolean strict, List files, RenameAction action, ConflictAction conflict, File output) throws Exception; + List rename(EpisodeListProvider db, String query, ExpressionFileFormat format, ExpressionFilter filter, SortOrder order, Locale locale, boolean strict, List files, RenameAction action, ConflictAction conflict, File output, ExecCommand exec) throws Exception; List rename(Map rename, RenameAction action, ConflictAction conflict) throws Exception; diff --git a/source/net/filebot/cli/CmdlineOperations.java b/source/net/filebot/cli/CmdlineOperations.java index 14481da7..b8392f4b 100644 --- a/source/net/filebot/cli/CmdlineOperations.java +++ b/source/net/filebot/cli/CmdlineOperations.java @@ -1,6 +1,7 @@ package net.filebot.cli; import static java.nio.charset.StandardCharsets.*; +import static java.util.Arrays.*; import static java.util.Collections.*; import static java.util.stream.Collectors.*; import static net.filebot.Logging.*; @@ -84,25 +85,25 @@ import net.filebot.web.VideoHashSubtitleService; public class CmdlineOperations implements CmdlineInterface { @Override - public List rename(Collection files, RenameAction action, ConflictAction conflict, File output, ExpressionFileFormat format, Datasource db, String query, SortOrder order, ExpressionFilter filter, Locale locale, boolean strict) throws Exception { + public List rename(Collection files, RenameAction action, ConflictAction conflict, File output, ExpressionFileFormat format, Datasource db, String query, SortOrder order, ExpressionFilter filter, Locale locale, boolean strict, ExecCommand exec) throws Exception { // movie mode if (db instanceof MovieIdentificationService) { - return renameMovie(files, action, conflict, output, format, (MovieIdentificationService) db, query, filter, locale, strict); + return renameMovie(files, action, conflict, output, format, (MovieIdentificationService) db, query, filter, locale, strict, exec); } // series mode if (db instanceof EpisodeListProvider) { - return renameSeries(files, action, conflict, output, format, (EpisodeListProvider) db, query, order, filter, locale, strict); + return renameSeries(files, action, conflict, output, format, (EpisodeListProvider) db, query, order, filter, locale, strict, exec); } // music mode if (db instanceof MusicIdentificationService) { - return renameMusic(files, action, conflict, output, format, (MusicIdentificationService) db); + return renameMusic(files, action, conflict, output, format, singletonList((MusicIdentificationService) db), exec); } // generic file / xattr mode if (db instanceof XattrMetaInfoProvider) { - return renameFiles(files, action, conflict, output, format, (XattrMetaInfoProvider) db, filter, strict); + return renameFiles(files, action, conflict, output, format, (XattrMetaInfoProvider) db, filter, strict, exec); } // auto-detect mode for each fileset @@ -114,16 +115,16 @@ public class CmdlineOperations implements CmdlineInterface { for (Type key : it.getKey().types()) { switch (key) { case Movie: - results.addAll(renameMovie(it.getValue(), action, conflict, output, format, TheMovieDB, query, filter, locale, strict)); + results.addAll(renameMovie(it.getValue(), action, conflict, output, format, TheMovieDB, query, filter, locale, strict, exec)); break; case Series: - results.addAll(renameSeries(it.getValue(), action, conflict, output, format, TheTVDB, query, order, filter, locale, strict)); + results.addAll(renameSeries(it.getValue(), action, conflict, output, format, TheTVDB, query, order, filter, locale, strict, exec)); break; case Anime: - results.addAll(renameSeries(it.getValue(), action, conflict, output, format, AniDB, query, order, filter, locale, strict)); + results.addAll(renameSeries(it.getValue(), action, conflict, output, format, AniDB, query, order, filter, locale, strict, exec)); break; case Music: - results.addAll(renameMusic(it.getValue(), action, conflict, output, format, MediaInfoID3, AcoustID)); + results.addAll(renameMusic(it.getValue(), action, conflict, output, format, asList(MediaInfoID3, AcoustID), exec)); // prefer existing ID3 tags and use acoustid only when necessary break; } } @@ -140,7 +141,7 @@ public class CmdlineOperations implements CmdlineInterface { } @Override - public List rename(EpisodeListProvider db, String query, ExpressionFileFormat format, ExpressionFilter filter, SortOrder order, Locale locale, boolean strict, List files, RenameAction action, ConflictAction conflict, File outputDir) throws Exception { + public List rename(EpisodeListProvider db, String query, ExpressionFileFormat format, ExpressionFilter filter, SortOrder order, Locale locale, boolean strict, List files, RenameAction action, ConflictAction conflict, File outputDir, ExecCommand exec) throws Exception { // match files and episodes in linear order List episodes = fetchEpisodeList(db, query, filter, order, locale, strict); @@ -150,16 +151,16 @@ public class CmdlineOperations implements CmdlineInterface { } // rename episodes - return renameAll(formatMatches(matches, format, outputDir), action, conflict, matches); + return renameAll(formatMatches(matches, format, outputDir), action, conflict, matches, exec); } @Override public List rename(Map renameMap, RenameAction renameAction, ConflictAction conflict) throws Exception { // generic rename function that can be passed any set of files - return renameAll(renameMap, renameAction, conflict, null); + return renameAll(renameMap, renameAction, conflict, null, null); } - public List renameSeries(Collection files, RenameAction renameAction, ConflictAction conflictAction, File outputDir, ExpressionFileFormat format, EpisodeListProvider db, String query, SortOrder sortOrder, ExpressionFilter filter, Locale locale, boolean strict) throws Exception { + public List renameSeries(Collection files, RenameAction renameAction, ConflictAction conflictAction, File outputDir, ExpressionFileFormat format, EpisodeListProvider db, String query, SortOrder sortOrder, ExpressionFilter filter, Locale locale, boolean strict, ExecCommand exec) throws Exception { log.config(format("Rename episodes using [%s]", db.getName())); // ignore sample files @@ -242,7 +243,7 @@ public class CmdlineOperations implements CmdlineInterface { matches.addAll(derivateMatches); // rename episodes - return renameAll(formatMatches(matches, format, outputDir), renameAction, conflictAction, matches); + return renameAll(formatMatches(matches, format, outputDir), renameAction, conflictAction, matches, exec); } private List> matchEpisodes(Collection files, Collection episodes, boolean strict) throws Exception { @@ -303,7 +304,7 @@ public class CmdlineOperations implements CmdlineInterface { return new ArrayList(episodes); } - public List renameMovie(Collection files, RenameAction renameAction, ConflictAction conflictAction, File outputDir, ExpressionFileFormat format, MovieIdentificationService service, String query, ExpressionFilter filter, Locale locale, boolean strict) throws Exception { + public List renameMovie(Collection files, RenameAction renameAction, ConflictAction conflictAction, File outputDir, ExpressionFileFormat format, MovieIdentificationService service, String query, ExpressionFilter filter, Locale locale, boolean strict, ExecCommand exec) throws Exception { log.config(format("Rename movies using [%s]", service.getName())); // ignore sample files @@ -480,10 +481,10 @@ public class CmdlineOperations implements CmdlineInterface { }); // rename movies - return renameAll(formatMatches(matches, format, outputDir), renameAction, conflictAction, matches); + return renameAll(formatMatches(matches, format, outputDir), renameAction, conflictAction, matches, exec); } - public List renameMusic(Collection files, RenameAction renameAction, ConflictAction conflictAction, File outputDir, ExpressionFileFormat format, MusicIdentificationService... services) throws Exception { + public List renameMusic(Collection files, RenameAction renameAction, ConflictAction conflictAction, File outputDir, ExpressionFileFormat format, List services, ExecCommand exec) throws Exception { List audioFiles = sortByUniquePath(filter(files, AUDIO_FILES, VIDEO_FILES)); // check audio files against all services if necessary @@ -491,24 +492,26 @@ public class CmdlineOperations implements CmdlineInterface { LinkedHashSet remaining = new LinkedHashSet(audioFiles); // check audio files against all services - for (int i = 0; i < services.length && remaining.size() > 0; i++) { - log.config(format("Rename music using %s", services[i].getIdentifier())); - services[i].lookup(remaining).forEach((file, music) -> { - if (music != null) { - matches.add(new Match(file, music.clone())); - remaining.remove(file); - } - }); + for (MusicIdentificationService service : services) { + if (remaining.size() > 0) { + log.config(format("Rename music using %s", service.getIdentifier())); + service.lookup(remaining).forEach((file, music) -> { + if (music != null) { + matches.add(new Match(file, music.clone())); + remaining.remove(file); + } + }); + } } // error logging remaining.forEach(f -> log.warning(format("Failed to process music file: %s", f))); // rename movies - return renameAll(formatMatches(matches, format, outputDir), renameAction, conflictAction, null); + return renameAll(formatMatches(matches, format, outputDir), renameAction, conflictAction, null, exec); } - public List renameFiles(Collection files, RenameAction renameAction, ConflictAction conflictAction, File outputDir, ExpressionFileFormat format, XattrMetaInfoProvider service, ExpressionFilter filter, boolean strict) throws Exception { + public List renameFiles(Collection files, RenameAction renameAction, ConflictAction conflictAction, File outputDir, ExpressionFileFormat format, XattrMetaInfoProvider service, ExpressionFilter filter, boolean strict, ExecCommand exec) throws Exception { log.config(format("Rename files using [%s]", service.getName())); Map renameMap = new LinkedHashMap(); @@ -525,7 +528,7 @@ public class CmdlineOperations implements CmdlineInterface { } }); - return renameAll(renameMap, renameAction, conflictAction, null); + return renameAll(renameMap, renameAction, conflictAction, null, exec); } private Map getContext(List> matches) { @@ -533,7 +536,7 @@ public class CmdlineOperations implements CmdlineInterface { @Override public Set> entrySet() { - return matches.stream().collect(toMap(it -> it.getValue(), it -> (Object) it.getCandidate())).entrySet(); + return matches.stream().collect(toMap(it -> it.getValue(), it -> (Object) it.getCandidate(), (a, b) -> a, LinkedHashMap::new)).entrySet(); } }; } @@ -570,7 +573,7 @@ public class CmdlineOperations implements CmdlineInterface { return renameMap; } - protected List renameAll(Map renameMap, RenameAction renameAction, ConflictAction conflictAction, List> matches) throws Exception { + protected List renameAll(Map renameMap, RenameAction renameAction, ConflictAction conflictAction, List> matches, ExecCommand exec) throws Exception { if (renameMap.isEmpty()) { throw new CmdlineException("Failed to identify or process any files"); } @@ -596,7 +599,7 @@ public class CmdlineOperations implements CmdlineInterface { } // do not allow abuse of online databases by repeatedly processing the same files - if (matches != null && equalsFileContent(source, destination)) { + if (matches != null && renameAction.canRevert() && source.length() > 0 && equalsFileContent(source, destination)) { throw new CmdlineException(String.format("Failed to process [%s] because [%s] is an exact copy and already exists", source, destination)); } @@ -636,33 +639,39 @@ public class CmdlineOperations implements CmdlineInterface { } } finally { // update history and xattr metadata - writeHistory(renameAction, renameLog, matches); + if (renameLog.size() > 0) { + writeHistory(renameAction, renameLog, matches); + } // print number of processed files log.fine(format("Processed %d files", renameLog.size())); } - // new file names + // execute command + if (exec != null) { + Map context = renameLog.values().stream().filter(Objects::nonNull).collect(toMap(f -> f, f -> xattr.getMetaInfo(f), (a, b) -> a, LinkedHashMap::new)); + if (context.size() > 0) { + exec.execute(context.entrySet().stream().map(m -> new MediaBindingBean(m.getValue(), m.getKey(), context)).toArray(MediaBindingBean[]::new)); + } + } + + // destination files may include null values return new ArrayList(renameLog.values()); } protected void writeHistory(RenameAction action, Map log, List> matches) { - if (log.isEmpty() || !action.canRevert()) { - return; - } - // write rename history - HistorySpooler.getInstance().append(log.entrySet()); + if (action.canRevert()) { + HistorySpooler.getInstance().append(log.entrySet()); + } // write xattr metadata if (matches != null) { for (Match match : matches) { - File source = match.getValue(); - Object infoObject = match.getCandidate(); - if (infoObject != null) { - File destination = log.get(source); + if (match.getCandidate() != null) { + File destination = log.get(match.getValue()); if (destination != null && destination.isFile()) { - xattr.setMetaInfo(destination, infoObject, source.getName()); + xattr.setMetaInfo(destination, match.getCandidate(), match.getValue().getName()); } } } diff --git a/source/net/filebot/cli/CmdlineOperationsTextUI.java b/source/net/filebot/cli/CmdlineOperationsTextUI.java index f1784c6e..5fbee5b1 100644 --- a/source/net/filebot/cli/CmdlineOperationsTextUI.java +++ b/source/net/filebot/cli/CmdlineOperationsTextUI.java @@ -69,10 +69,10 @@ public class CmdlineOperationsTextUI extends CmdlineOperations { } @Override - public List renameAll(Map renameMap, RenameAction renameAction, ConflictAction conflictAction, List> matches) throws Exception { + public List renameAll(Map renameMap, RenameAction renameAction, ConflictAction conflictAction, List> matches, ExecCommand exec) throws Exception { // default behavior if rename map is empty if (renameMap.isEmpty()) { - return super.renameAll(renameMap, renameAction, conflictAction, matches); + return super.renameAll(renameMap, renameAction, conflictAction, matches, exec); } // manually confirm each file mapping @@ -91,7 +91,7 @@ public class CmdlineOperationsTextUI extends CmdlineOperations { return emptyList(); } - return super.renameAll(selection.stream().collect(toMap(Entry::getKey, Entry::getValue, (a, b) -> a, LinkedHashMap::new)), renameAction, conflictAction, matches); + return super.renameAll(selection.stream().collect(toMap(Entry::getKey, Entry::getValue, (a, b) -> a, LinkedHashMap::new)), renameAction, conflictAction, matches, exec); } @Override diff --git a/source/net/filebot/cli/ExecCommand.java b/source/net/filebot/cli/ExecCommand.java new file mode 100644 index 00000000..a16953d1 --- /dev/null +++ b/source/net/filebot/cli/ExecCommand.java @@ -0,0 +1,99 @@ +package net.filebot.cli; + +import static java.util.stream.Collectors.*; +import static net.filebot.Logging.*; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +import javax.script.ScriptException; + +import net.filebot.format.ExpressionFormat; +import net.filebot.format.MediaBindingBean; + +public class ExecCommand { + + private List template; + private boolean parallel; + + private File directory; + + public ExecCommand(List template, boolean parallel, File directory) { + this.template = template; + this.parallel = parallel; + this.directory = directory; + } + + public void execute(MediaBindingBean... group) throws IOException, InterruptedException { + if (parallel) { + executeParallel(group); + } else { + executeSequence(group); + } + } + + private void executeSequence(MediaBindingBean... group) throws IOException, InterruptedException { + // collect unique commands + List> commands = Stream.of(group).map(v -> { + return template.stream().map(t -> getArgumentValue(t, v)).filter(Objects::nonNull).collect(toList()); + }).distinct().collect(toList()); + + // execute unique commands + for (List command : commands) { + execute(command); + } + } + + private void executeParallel(MediaBindingBean... group) throws IOException, InterruptedException { + // collect single command + List command = template.stream().flatMap(t -> { + return Stream.of(group).map(v -> getArgumentValue(t, v)).filter(Objects::nonNull).distinct(); + }).collect(toList()); + + // execute single command + execute(command); + } + + private String getArgumentValue(ExpressionFormat template, MediaBindingBean variables) { + try { + return template.format(variables); + } catch (Exception e) { + debug.warning(cause(template.getExpression(), e)); + } + return null; + } + + private void execute(List command) throws IOException, InterruptedException { + ProcessBuilder process = new ProcessBuilder(command); + process.directory(directory); + process.inheritIO(); + + debug.finest(message("Execute", command)); + + int exitCode = process.start().waitFor(); + if (exitCode != 0) { + throw new IOException(String.format("%s failed with exit code %d", command, exitCode)); + } + } + + public static ExecCommand parse(List args, File directory) throws ScriptException { + // execute one command per file or one command with many file arguments + boolean parallel = args.lastIndexOf("+") == args.size() - 1; + + if (parallel) { + args = args.subList(0, args.size() - 1); + } + + List template = new ArrayList(); + for (String argument : args) { + template.add(new ExpressionFormat(argument)); + } + + return new ExecCommand(template, parallel, directory); + } + +} diff --git a/source/net/filebot/cli/ScriptShellBaseClass.java b/source/net/filebot/cli/ScriptShellBaseClass.java index 73b220d0..fcf09346 100644 --- a/source/net/filebot/cli/ScriptShellBaseClass.java +++ b/source/net/filebot/cli/ScriptShellBaseClass.java @@ -339,7 +339,7 @@ public abstract class ScriptShellBaseClass extends Script { try { if (files.size() > 0) { - return getCLI().rename(files, action, args.getConflictAction(), args.getAbsoluteOutputFolder(), args.getExpressionFileFormat(), args.getDatasource(), args.getSearchQuery(), args.getSortOrder(), args.getExpressionFilter(), args.getLanguage().getLocale(), args.isStrict()); + return getCLI().rename(files, action, args.getConflictAction(), args.getAbsoluteOutputFolder(), args.getExpressionFileFormat(), args.getDatasource(), args.getSearchQuery(), args.getSortOrder(), args.getExpressionFilter(), args.getLanguage().getLocale(), args.isStrict(), args.getExecCommand()); } if (map.size() > 0) {