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} +
```
This commit is contained in:
Reinhard Pointner 2017-04-18 15:25:34 +08:00
parent 789c472876
commit aa10510e87
7 changed files with 171 additions and 51 deletions

View File

@ -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<String, String> defines = new LinkedHashMap<String, String>();
@Option(name = "-exec", usage = "Execute command", handler = RestOfArgumentsHandler.class)
public List<String> exec = new ArrayList<String>();
@Argument
public List<String> arguments = new ArrayList<String>();
@ -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) {

View File

@ -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) {

View File

@ -23,9 +23,9 @@ import net.filebot.web.SortOrder;
public interface CmdlineInterface {
List<File> rename(Collection<File> files, RenameAction action, ConflictAction conflict, File output, ExpressionFileFormat format, Datasource db, String query, SortOrder order, ExpressionFilter filter, Locale locale, boolean strict) throws Exception;
List<File> rename(Collection<File> 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<File> rename(EpisodeListProvider db, String query, ExpressionFileFormat format, ExpressionFilter filter, SortOrder order, Locale locale, boolean strict, List<File> files, RenameAction action, ConflictAction conflict, File output) throws Exception;
List<File> rename(EpisodeListProvider db, String query, ExpressionFileFormat format, ExpressionFilter filter, SortOrder order, Locale locale, boolean strict, List<File> files, RenameAction action, ConflictAction conflict, File output, ExecCommand exec) throws Exception;
List<File> rename(Map<File, File> rename, RenameAction action, ConflictAction conflict) throws Exception;

View File

@ -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<File> rename(Collection<File> 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<File> rename(Collection<File> 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<File> rename(EpisodeListProvider db, String query, ExpressionFileFormat format, ExpressionFilter filter, SortOrder order, Locale locale, boolean strict, List<File> files, RenameAction action, ConflictAction conflict, File outputDir) throws Exception {
public List<File> rename(EpisodeListProvider db, String query, ExpressionFileFormat format, ExpressionFilter filter, SortOrder order, Locale locale, boolean strict, List<File> files, RenameAction action, ConflictAction conflict, File outputDir, ExecCommand exec) throws Exception {
// match files and episodes in linear order
List<Episode> 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<File> rename(Map<File, File> 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<File> renameSeries(Collection<File> 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<File> renameSeries(Collection<File> 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<Match<File, Object>> matchEpisodes(Collection<File> files, Collection<Episode> episodes, boolean strict) throws Exception {
@ -303,7 +304,7 @@ public class CmdlineOperations implements CmdlineInterface {
return new ArrayList<Episode>(episodes);
}
public List<File> renameMovie(Collection<File> files, RenameAction renameAction, ConflictAction conflictAction, File outputDir, ExpressionFileFormat format, MovieIdentificationService service, String query, ExpressionFilter filter, Locale locale, boolean strict) throws Exception {
public List<File> renameMovie(Collection<File> 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<File> renameMusic(Collection<File> files, RenameAction renameAction, ConflictAction conflictAction, File outputDir, ExpressionFileFormat format, MusicIdentificationService... services) throws Exception {
public List<File> renameMusic(Collection<File> files, RenameAction renameAction, ConflictAction conflictAction, File outputDir, ExpressionFileFormat format, List<MusicIdentificationService> services, ExecCommand exec) throws Exception {
List<File> 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<File> remaining = new LinkedHashSet<File>(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, AudioTrack>(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, AudioTrack>(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<File> renameFiles(Collection<File> files, RenameAction renameAction, ConflictAction conflictAction, File outputDir, ExpressionFileFormat format, XattrMetaInfoProvider service, ExpressionFilter filter, boolean strict) throws Exception {
public List<File> renameFiles(Collection<File> 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<File, File> renameMap = new LinkedHashMap<File, File>();
@ -525,7 +528,7 @@ public class CmdlineOperations implements CmdlineInterface {
}
});
return renameAll(renameMap, renameAction, conflictAction, null);
return renameAll(renameMap, renameAction, conflictAction, null, exec);
}
private Map<File, Object> getContext(List<Match<File, ?>> matches) {
@ -533,7 +536,7 @@ public class CmdlineOperations implements CmdlineInterface {
@Override
public Set<Entry<File, Object>> 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<File> renameAll(Map<File, File> renameMap, RenameAction renameAction, ConflictAction conflictAction, List<Match<File, ?>> matches) throws Exception {
protected List<File> renameAll(Map<File, File> renameMap, RenameAction renameAction, ConflictAction conflictAction, List<Match<File, ?>> 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<File, Object> 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<File>(renameLog.values());
}
protected void writeHistory(RenameAction action, Map<File, File> log, List<Match<File, ?>> 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<File, ?> 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());
}
}
}

View File

@ -69,10 +69,10 @@ public class CmdlineOperationsTextUI extends CmdlineOperations {
}
@Override
public List<File> renameAll(Map<File, File> renameMap, RenameAction renameAction, ConflictAction conflictAction, List<Match<File, ?>> matches) throws Exception {
public List<File> renameAll(Map<File, File> renameMap, RenameAction renameAction, ConflictAction conflictAction, List<Match<File, ?>> 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

View File

@ -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<ExpressionFormat> template;
private boolean parallel;
private File directory;
public ExecCommand(List<ExpressionFormat> 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<List<String>> 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<String> command : commands) {
execute(command);
}
}
private void executeParallel(MediaBindingBean... group) throws IOException, InterruptedException {
// collect single command
List<String> 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<String> 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<String> 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<ExpressionFormat> template = new ArrayList<ExpressionFormat>();
for (String argument : args) {
template.add(new ExpressionFormat(argument));
}
return new ExecCommand(template, parallel, directory);
}
}

View File

@ -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) {