diff --git a/.classpath b/.classpath
index 6a484a24..cec9ef29 100644
--- a/.classpath
+++ b/.classpath
@@ -34,5 +34,6 @@
+
diff --git a/build.xml b/build.xml
index e0e6c04b..0bcbaf0b 100644
--- a/build.xml
+++ b/build.xml
@@ -253,6 +253,11 @@
+
+
+
+
+
diff --git a/ivy.xml b/ivy.xml
index 2885600b..f97f35ed 100644
--- a/ivy.xml
+++ b/ivy.xml
@@ -25,6 +25,7 @@
+
diff --git a/source/net/filebot/RenameAction.java b/source/net/filebot/RenameAction.java
index 2b56978b..959dd922 100644
--- a/source/net/filebot/RenameAction.java
+++ b/source/net/filebot/RenameAction.java
@@ -1,12 +1,14 @@
package net.filebot;
-
import java.io.File;
-
public interface RenameAction {
File rename(File from, File to) throws Exception;
+ default boolean canRevert() {
+ return true;
+ }
+
}
diff --git a/source/net/filebot/StandardRenameAction.java b/source/net/filebot/StandardRenameAction.java
index f149abc0..8e4e72ac 100644
--- a/source/net/filebot/StandardRenameAction.java
+++ b/source/net/filebot/StandardRenameAction.java
@@ -129,6 +129,11 @@ public enum StandardRenameAction implements RenameAction {
public File rename(File from, File to) throws IOException {
return FileUtilities.resolve(from, to);
}
+
+ @Override
+ public boolean canRevert() {
+ return false;
+ }
};
public String getDisplayName() {
diff --git a/source/net/filebot/cli/ArgumentBean.java b/source/net/filebot/cli/ArgumentBean.java
index 9719fd5f..17ffbd53 100644
--- a/source/net/filebot/cli/ArgumentBean.java
+++ b/source/net/filebot/cli/ArgumentBean.java
@@ -124,6 +124,10 @@ public class ArgumentBean {
return rename || getSubtitles || check || list || mediaInfo || revert || extract || script != null;
}
+ public boolean isInteractive() {
+ return "interactive".equalsIgnoreCase(mode) && System.console() != null;
+ }
+
public boolean printVersion() {
return version;
}
diff --git a/source/net/filebot/cli/ArgumentProcessor.java b/source/net/filebot/cli/ArgumentProcessor.java
index ca999858..a85d36df 100644
--- a/source/net/filebot/cli/ArgumentProcessor.java
+++ b/source/net/filebot/cli/ArgumentProcessor.java
@@ -20,12 +20,15 @@ public class ArgumentProcessor {
public int run(ArgumentBean args) {
try {
+ // interactive mode enables basic selection and confirmation dialogs in the CLI
+ CmdlineInterface cli = args.isInteractive() ? new CmdlineOperationsTextUI() : new CmdlineOperations();
+
if (args.script == null) {
// execute command
- return runCommand(args);
+ return runCommand(cli, args);
} else {
// execute user script
- runScript(args);
+ runScript(cli, args);
// script finished successfully
log.finest("Done ヾ(@⌒ー⌒@)ノ");
@@ -46,9 +49,7 @@ public class ArgumentProcessor {
return 1;
}
- public int runCommand(ArgumentBean args) throws Exception {
- CmdlineInterface cli = new CmdlineOperations();
-
+ public int runCommand(CmdlineInterface cli, ArgumentBean args) throws Exception {
// sanity checks
if (args.getSubtitles && args.recursive) {
throw new CmdlineException("`filebot -get-subtitles -r` has been disabled due to abuse. Please see http://bit.ly/suball for details.");
@@ -103,13 +104,13 @@ public class ArgumentProcessor {
return 0;
}
- public void runScript(ArgumentBean args) throws Throwable {
+ public void runScript(CmdlineInterface cli, ArgumentBean args) throws Throwable {
Bindings bindings = new SimpleBindings();
- bindings.put(ScriptShell.SHELL_ARGV_BINDING_NAME, args.getArgumentArray());
+ bindings.put(ScriptShell.SHELL_ARGS_BINDING_NAME, args);
bindings.put(ScriptShell.ARGV_BINDING_NAME, args.getFiles(false));
ScriptSource source = ScriptSource.findScriptProvider(args.script);
- ScriptShell shell = new ScriptShell(source.getScriptProvider(args.script), args.defines);
+ ScriptShell shell = new ScriptShell(source.getScriptProvider(args.script), cli, args.defines);
shell.runScript(source.accept(args.script), bindings);
}
diff --git a/source/net/filebot/cli/CmdlineOperations.java b/source/net/filebot/cli/CmdlineOperations.java
index 35313742..6836306b 100644
--- a/source/net/filebot/cli/CmdlineOperations.java
+++ b/source/net/filebot/cli/CmdlineOperations.java
@@ -601,7 +601,7 @@ public class CmdlineOperations implements CmdlineInterface {
destination = resolve(source, destination);
}
- if (!destination.equals(source) && destination.exists() && renameAction != StandardRenameAction.TEST) {
+ if (!destination.equals(source) && destination.exists() && renameAction.canRevert()) {
if (conflictAction == ConflictAction.FAIL) {
throw new CmdlineException("File already exists: " + destination);
}
@@ -632,14 +632,16 @@ public class CmdlineOperations implements CmdlineInterface {
}
} finally {
// update rename history
- HistorySpooler.getInstance().append(renameLog.entrySet());
+ if (renameAction.canRevert()) {
+ HistorySpooler.getInstance().append(renameLog.entrySet());
+ }
// printer number of renamed files if any
log.fine(format("Processed %d files", renameLog.size()));
}
// write metadata into xattr if xattr is enabled
- if (matches != null && renameLog.size() > 0 && renameAction != StandardRenameAction.TEST) {
+ if (matches != null && renameLog.size() > 0 && renameAction.canRevert()) {
for (Match match : matches) {
File source = match.getValue();
Object infoObject = match.getCandidate();
@@ -868,7 +870,7 @@ public class CmdlineOperations implements CmdlineInterface {
return output;
}
- private List selectSearchResult(String query, Collection extends SearchResult> options, boolean alias, boolean strict) throws Exception {
+ protected List selectSearchResult(String query, Collection extends SearchResult> options, boolean alias, boolean strict) throws Exception {
List probableMatches = getProbableMatches(query, options, alias, strict);
if (probableMatches.isEmpty() || (strict && probableMatches.size() != 1)) {
diff --git a/source/net/filebot/cli/CmdlineOperationsTextUI.java b/source/net/filebot/cli/CmdlineOperationsTextUI.java
new file mode 100644
index 00000000..bf2bce4f
--- /dev/null
+++ b/source/net/filebot/cli/CmdlineOperationsTextUI.java
@@ -0,0 +1,162 @@
+package net.filebot.cli;
+
+import static java.util.Arrays.*;
+import static java.util.Collections.*;
+import static net.filebot.media.MediaDetection.*;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+
+import com.googlecode.lanterna.TextColor;
+import com.googlecode.lanterna.bundle.LanternaThemes;
+import com.googlecode.lanterna.gui2.BasicWindow;
+import com.googlecode.lanterna.gui2.Button;
+import com.googlecode.lanterna.gui2.CheckBoxList;
+import com.googlecode.lanterna.gui2.DefaultWindowManager;
+import com.googlecode.lanterna.gui2.Direction;
+import com.googlecode.lanterna.gui2.EmptySpace;
+import com.googlecode.lanterna.gui2.GridLayout;
+import com.googlecode.lanterna.gui2.LocalizedString;
+import com.googlecode.lanterna.gui2.MultiWindowTextGUI;
+import com.googlecode.lanterna.gui2.Panel;
+import com.googlecode.lanterna.gui2.Panels;
+import com.googlecode.lanterna.gui2.Separator;
+import com.googlecode.lanterna.gui2.Window.Hint;
+import com.googlecode.lanterna.gui2.dialogs.ListSelectDialogBuilder;
+import com.googlecode.lanterna.screen.Screen;
+import com.googlecode.lanterna.screen.TerminalScreen;
+import com.googlecode.lanterna.terminal.DefaultTerminalFactory;
+import com.googlecode.lanterna.terminal.Terminal;
+
+import net.filebot.RenameAction;
+import net.filebot.similarity.Match;
+import net.filebot.web.SearchResult;
+
+public class CmdlineOperationsTextUI extends CmdlineOperations {
+
+ public static final String DEFAULT_THEME = "businessmachine";
+
+ private Terminal terminal;
+ private Screen screen;
+ private MultiWindowTextGUI ui;
+
+ public CmdlineOperationsTextUI() throws Exception {
+ terminal = new DefaultTerminalFactory().createTerminal();
+ screen = new TerminalScreen(terminal);
+ ui = new MultiWindowTextGUI(screen, new DefaultWindowManager(), new EmptySpace(TextColor.ANSI.DEFAULT));
+
+ // use green matrix-style theme
+ ui.setTheme(LanternaThemes.getRegisteredTheme(DEFAULT_THEME));
+ }
+
+ public T onScreen(Supplier dialog) throws Exception {
+ try {
+ screen.startScreen();
+ return dialog.get();
+ } finally {
+ screen.stopScreen();
+ }
+ }
+
+ @Override
+ public List renameAll(Map renameMap, RenameAction renameAction, ConflictAction conflictAction, List> matches) throws Exception {
+ // manually confirm each file mapping
+ Map selection = onScreen(() -> confirmRenameMap(renameMap, renameAction, conflictAction));
+
+ return super.renameAll(selection, renameAction, conflictAction, matches);
+ }
+
+ @Override
+ protected List selectSearchResult(String query, Collection extends SearchResult> options, boolean alias, boolean strict) throws Exception {
+ List matches = getProbableMatches(query, options, alias, false);
+
+ // manually select option if there is more than one
+ if (matches.size() > 1) {
+ return onScreen(() -> confirmSearchResult(query, matches));
+ }
+
+ return matches;
+ }
+
+ protected List confirmSearchResult(String query, List options) {
+ ListSelectDialogBuilder dialog = new ListSelectDialogBuilder();
+ dialog.setTitle("Multiple Options");
+ dialog.setDescription(String.format("Select best match for \"%s\"", query));
+ dialog.setExtraWindowHints(singleton(Hint.CENTERED));
+
+ options.forEach(dialog::addListItem);
+
+ // show UI
+ SearchResult selection = dialog.build().showDialog(ui);
+ if (selection == null) {
+ return emptyList();
+ }
+
+ return singletonList(selection);
+ }
+
+ protected Map confirmRenameMap(Map renameMap, RenameAction renameAction, ConflictAction conflictAction) {
+ Map selection = new LinkedHashMap();
+
+ BasicWindow dialog = new BasicWindow();
+ dialog.setTitle(String.format("%s / %s", renameAction, conflictAction));
+ dialog.setHints(asList(Hint.MODAL, Hint.CENTERED));
+
+ CheckBoxList checkBoxList = new CheckBoxList();
+
+ int columnSize = renameMap.keySet().stream().mapToInt(f -> f.getName().length()).max().orElse(0);
+ String labelFormat = "%-" + columnSize + "s\t=>\t%s";
+
+ renameMap.forEach((k, v) -> {
+ checkBoxList.addItem(new CheckBoxListItem(String.format(labelFormat, k.getName(), v.getName()), k, v), true);
+ });
+
+ Button continueButton = new Button(LocalizedString.OK.toString(), () -> {
+ checkBoxList.getCheckedItems().forEach(it -> selection.put(it.key, it.value));
+ dialog.close();
+ });
+
+ Button cancelButton = new Button(LocalizedString.Cancel.toString(), () -> {
+ selection.clear();
+ dialog.close();
+ });
+
+ Panel contentPane = new Panel();
+ contentPane.setLayoutManager(new GridLayout(1));
+
+ contentPane.addComponent(checkBoxList.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.BEGINNING, GridLayout.Alignment.BEGINNING, true, true, 1, 1)));
+ contentPane.addComponent(new Separator(Direction.HORIZONTAL).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.FILL, GridLayout.Alignment.CENTER, true, false, 1, 1)));
+ contentPane.addComponent(Panels.grid(2, continueButton, cancelButton).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER, false, false, 1, 1)));
+
+ dialog.setComponent(contentPane);
+
+ ui.addWindowAndWait(dialog);
+
+ return selection;
+ }
+
+ protected static class CheckBoxListItem {
+
+ public final String label;
+
+ public final File key;
+ public final File value;
+
+ public CheckBoxListItem(String label, File key, File value) {
+ this.label = label;
+ this.key = key;
+ this.value = value;
+ }
+
+ @Override
+ public String toString() {
+ return label;
+ }
+
+ }
+
+}
diff --git a/source/net/filebot/cli/GroovyPad.java b/source/net/filebot/cli/GroovyPad.java
index c39eac4b..2e3b1df6 100644
--- a/source/net/filebot/cli/GroovyPad.java
+++ b/source/net/filebot/cli/GroovyPad.java
@@ -160,7 +160,7 @@ public class GroovyPad extends JFrame {
protected ScriptShell createScriptShell() {
try {
- return new ScriptShell(s -> ScriptSource.GITHUB_STABLE.getScriptProvider(s).getScript(s), new HashMap());
+ return new ScriptShell(s -> ScriptSource.GITHUB_STABLE.getScriptProvider(s).getScript(s), new CmdlineOperations(), new HashMap());
} catch (Exception e) {
throw new RuntimeException(e);
}
@@ -226,7 +226,7 @@ public class GroovyPad extends JFrame {
public void run() {
try {
Bindings bindings = new SimpleBindings();
- bindings.put(ScriptShell.SHELL_ARGV_BINDING_NAME, Settings.getApplicationArguments().getArgumentArray());
+ bindings.put(ScriptShell.SHELL_ARGS_BINDING_NAME, Settings.getApplicationArguments());
bindings.put(ScriptShell.ARGV_BINDING_NAME, Settings.getApplicationArguments().getFiles(false));
result = shell.evaluate(script, bindings);
diff --git a/source/net/filebot/cli/ScriptShell.java b/source/net/filebot/cli/ScriptShell.java
index 5e27e310..ac5fadc5 100644
--- a/source/net/filebot/cli/ScriptShell.java
+++ b/source/net/filebot/cli/ScriptShell.java
@@ -21,12 +21,13 @@ public class ScriptShell {
public static final String ARGV_BINDING_NAME = "args";
public static final String SHELL_BINDING_NAME = "__shell";
- public static final String SHELL_ARGV_BINDING_NAME = "__args";
+ public static final String SHELL_CLI_BINDING_NAME = "__cli";
+ public static final String SHELL_ARGS_BINDING_NAME = "__args";
private final ScriptEngine engine;
private final ScriptProvider scriptProvider;
- public ScriptShell(ScriptProvider scriptProvider, Map globals) throws ScriptException {
+ public ScriptShell(ScriptProvider scriptProvider, CmdlineInterface cli, Map globals) throws ScriptException {
this.engine = createScriptEngine();
this.scriptProvider = scriptProvider;
@@ -36,6 +37,7 @@ public class ScriptShell {
// bind API objects
bindings.put(SHELL_BINDING_NAME, this);
+ bindings.put(SHELL_CLI_BINDING_NAME, cli);
// setup script context
engine.getContext().setBindings(bindings, ScriptContext.GLOBAL_SCOPE);
diff --git a/source/net/filebot/cli/ScriptShellBaseClass.java b/source/net/filebot/cli/ScriptShellBaseClass.java
index 232ea5f5..94759b3a 100644
--- a/source/net/filebot/cli/ScriptShellBaseClass.java
+++ b/source/net/filebot/cli/ScriptShellBaseClass.java
@@ -78,6 +78,18 @@ public abstract class ScriptShellBaseClass extends Script {
}
}
+ private ArgumentBean getArgumentBean() {
+ return (ArgumentBean) getBinding().getVariable(ScriptShell.SHELL_ARGS_BINDING_NAME);
+ }
+
+ private ScriptShell getShell() {
+ return (ScriptShell) getBinding().getVariable(ScriptShell.SHELL_BINDING_NAME);
+ }
+
+ private CmdlineInterface getCLI() {
+ return (CmdlineInterface) getBinding().getVariable(ScriptShell.SHELL_CLI_BINDING_NAME);
+ }
+
public void include(String input) throws Throwable {
try {
executeScript(input, null, null, null);
@@ -109,12 +121,11 @@ public abstract class ScriptShellBaseClass extends Script {
parameters.putAll(bindings);
}
- parameters.put(ScriptShell.SHELL_ARGV_BINDING_NAME, argv != null ? argv.toArray(new String[0]) : new String[0]);
+ parameters.put(ScriptShell.SHELL_ARGS_BINDING_NAME, new ArgumentBean(argv != null ? argv.toArray(new String[0]) : new String[0]));
parameters.put(ScriptShell.ARGV_BINDING_NAME, args != null ? new ArrayList(args) : new ArrayList());
// run given script
- ScriptShell shell = (ScriptShell) getBinding().getVariable(ScriptShell.SHELL_BINDING_NAME);
- return shell.runScript(input, parameters);
+ return getShell().runScript(input, parameters);
}
public Object tryQuietly(Closure> c) {
@@ -317,8 +328,6 @@ public abstract class ScriptShellBaseClass extends Script {
action, conflict, query, filter, format, db, order, lang, output, encoding, strict, forceExtractAll
}
- private static final CmdlineInterface cli = new CmdlineOperations();
-
public List rename(Map parameters) throws Exception {
List input = getInputFileList(parameters);
Map