diff --git a/source/ehcache.xml b/source/ehcache.xml
index 3cf7757b..9b7505a2 100644
--- a/source/ehcache.xml
+++ b/source/ehcache.xml
@@ -122,7 +122,7 @@
+
+
+
+
diff --git a/source/net/sourceforge/filebot/format/BindingException.java b/source/net/sourceforge/filebot/format/BindingException.java
new file mode 100644
index 00000000..c262b66a
--- /dev/null
+++ b/source/net/sourceforge/filebot/format/BindingException.java
@@ -0,0 +1,16 @@
+
+package net.sourceforge.filebot.format;
+
+
+public class BindingException extends RuntimeException {
+
+ public BindingException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+
+ public BindingException(String binding, String innerMessage, Throwable cause) {
+ this(String.format("BindingError: \"%s\": %s", binding, innerMessage), cause);
+ }
+
+}
diff --git a/source/net/sourceforge/filebot/format/Define.java b/source/net/sourceforge/filebot/format/Define.java
new file mode 100644
index 00000000..db9bbaef
--- /dev/null
+++ b/source/net/sourceforge/filebot/format/Define.java
@@ -0,0 +1,19 @@
+
+package net.sourceforge.filebot.format;
+
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+
+@Retention(RUNTIME)
+@Target(METHOD)
+public @interface Define {
+
+ String[] value();
+
+ static final String undefined = "";
+}
diff --git a/source/net/sourceforge/filebot/format/EpisodeExpressionFormat.java b/source/net/sourceforge/filebot/format/EpisodeExpressionFormat.java
new file mode 100644
index 00000000..c796801f
--- /dev/null
+++ b/source/net/sourceforge/filebot/format/EpisodeExpressionFormat.java
@@ -0,0 +1,41 @@
+
+package net.sourceforge.filebot.format;
+
+
+import java.io.File;
+
+import javax.script.Bindings;
+import javax.script.ScriptException;
+
+import net.sourceforge.filebot.similarity.Match;
+import net.sourceforge.filebot.web.Episode;
+
+
+public class EpisodeExpressionFormat extends ExpressionFormat {
+
+ public EpisodeExpressionFormat(String format) throws ScriptException {
+ super(format);
+ }
+
+
+ @Override
+ public Bindings getBindings(Object value) {
+ @SuppressWarnings("unchecked")
+ Match match = (Match) value;
+
+ return new ExpressionBindings(new EpisodeFormatBindingBean(match.getValue(), match.getCandidate()));
+ }
+
+
+ @Override
+ protected void dispose(Bindings bindings) {
+ // dispose binding bean
+ getBindingBean(bindings).dispose();
+ }
+
+
+ private EpisodeFormatBindingBean getBindingBean(Bindings bindings) {
+ return (EpisodeFormatBindingBean) ((ExpressionBindings) bindings).getBindingBean();
+ }
+
+}
diff --git a/source/net/sourceforge/filebot/format/EpisodeFormatBindingBean.java b/source/net/sourceforge/filebot/format/EpisodeFormatBindingBean.java
new file mode 100644
index 00000000..686d1f2c
--- /dev/null
+++ b/source/net/sourceforge/filebot/format/EpisodeFormatBindingBean.java
@@ -0,0 +1,214 @@
+
+package net.sourceforge.filebot.format;
+
+
+import static net.sourceforge.filebot.format.Define.undefined;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.SortedMap;
+import java.util.zip.CRC32;
+
+import net.sf.ehcache.Cache;
+import net.sf.ehcache.CacheManager;
+import net.sf.ehcache.Element;
+import net.sourceforge.filebot.FileBotUtilities;
+import net.sourceforge.filebot.mediainfo.MediaInfo;
+import net.sourceforge.filebot.mediainfo.MediaInfo.StreamKind;
+import net.sourceforge.filebot.web.Episode;
+
+
+public class EpisodeFormatBindingBean {
+
+ private final Episode episode;
+
+ private final File mediaFile;
+
+ private MediaInfo mediaInfo;
+
+
+ public EpisodeFormatBindingBean(Episode episode, File mediaFile) {
+ this.episode = episode;
+ this.mediaFile = mediaFile;
+ }
+
+
+ @Define(undefined)
+ public String undefined() {
+ // omit expressions that depend on undefined values
+ throw new RuntimeException("undefined");
+ }
+
+
+ @Define("n")
+ public String getSeriesName() {
+ return episode.getSeriesName();
+ }
+
+
+ @Define("s")
+ public String getSeasonNumber() {
+ return episode.getSeasonNumber();
+ }
+
+
+ @Define("e")
+ public String getEpisodeNumber() {
+ return episode.getEpisodeNumber();
+ }
+
+
+ @Define("t")
+ public String getTitle() {
+ return episode.getTitle();
+ }
+
+
+ @Define("vc")
+ public String getVideoCodec() {
+ return getMediaInfo(StreamKind.Video, 0, "Encoded_Library/Name", "CodecID/Hint", "Codec/String");
+ }
+
+
+ @Define("ac")
+ public String getAudioCodec() {
+ return getMediaInfo(StreamKind.Audio, 0, "CodecID/Hint", "Codec/String");
+ }
+
+
+ @Define("hi")
+ public String getHeightAndInterlacement() {
+ String height = getMediaInfo(StreamKind.Video, 0, "Height");
+ String interlacement = getMediaInfo(StreamKind.Video, 0, "Interlacement");
+
+ if (height == null || interlacement == null)
+ return null;
+
+ // e.g. 720p
+ return height + Character.toLowerCase(interlacement.charAt(0));
+ }
+
+
+ @Define("resolution")
+ public String getVideoResolution() {
+ String width = getMediaInfo(StreamKind.Video, 0, "Width");
+ String height = getMediaInfo(StreamKind.Video, 0, "Height");
+
+ if (width == null || height == null)
+ return null;
+
+ // e.g. 1280x720
+ return width + 'x' + height;
+ }
+
+
+ @Define("crc32")
+ public String getCRC32() throws IOException {
+ if (mediaFile != null) {
+ // try to get checksum from file name
+ String embeddedChecksum = FileBotUtilities.getEmbeddedChecksum(mediaFile.getName());
+
+ if (embeddedChecksum != null) {
+ return embeddedChecksum;
+ }
+
+ // calculate checksum from file
+ return crc32(mediaFile);
+ }
+
+ return null;
+ }
+
+
+ @Define("video")
+ public SortedMap getVideoInfo() {
+ return getMediaInfo().snapshot(StreamKind.Video, 0);
+ }
+
+
+ @Define("audio")
+ public SortedMap getAudioInfo() {
+ return getMediaInfo().snapshot(StreamKind.Audio, 0);
+ }
+
+
+ @Define("general")
+ public SortedMap getGeneralMediaInfo() {
+ return getMediaInfo().snapshot(StreamKind.General, 0);
+ }
+
+
+ public synchronized MediaInfo getMediaInfo() {
+ if (mediaInfo == null) {
+ mediaInfo = new MediaInfo();
+
+ if (mediaFile == null || !mediaInfo.open(mediaFile)) {
+ throw new RuntimeException(String.format("Cannot open file: %s", mediaFile));
+ }
+ }
+
+ return mediaInfo;
+ }
+
+
+ public synchronized void dispose() {
+ if (mediaInfo != null) {
+ mediaInfo.close();
+ mediaInfo.dispose();
+ }
+
+ mediaInfo = null;
+ }
+
+
+ private String getMediaInfo(StreamKind streamKind, int streamNumber, String... keys) {
+ MediaInfo mediaInfo = getMediaInfo();
+
+ if (mediaInfo != null) {
+ for (String key : keys) {
+ String value = mediaInfo.get(streamKind, streamNumber, key);
+
+ if (value.length() > 0) {
+ return value;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private static final Cache checksumCache = CacheManager.getInstance().getCache("checksum");
+
+
+ private String crc32(File file) throws IOException {
+ // try to get checksum from cache
+ Element cacheEntry = checksumCache.get(file);
+
+ if (cacheEntry != null) {
+ return (String) cacheEntry.getValue();
+ }
+
+ // calculate checksum
+ InputStream in = new FileInputStream(file);
+ CRC32 crc = new CRC32();
+
+ try {
+ byte[] buffer = new byte[32 * 1024];
+ int len = 0;
+
+ while ((len = in.read(buffer)) >= 0) {
+ crc.update(buffer, 0, len);
+ }
+ } finally {
+ in.close();
+ }
+
+ String checksum = String.format("%08X", crc.getValue());
+
+ checksumCache.put(new Element(file, checksum));
+ return checksum;
+ }
+
+}
diff --git a/source/net/sourceforge/filebot/format/ExpressionBindings.java b/source/net/sourceforge/filebot/format/ExpressionBindings.java
new file mode 100644
index 00000000..9e01fad3
--- /dev/null
+++ b/source/net/sourceforge/filebot/format/ExpressionBindings.java
@@ -0,0 +1,137 @@
+
+package net.sourceforge.filebot.format;
+
+
+import java.lang.reflect.Method;
+import java.util.AbstractMap;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import javax.script.Bindings;
+
+import net.sourceforge.tuned.ExceptionUtilities;
+
+
+public class ExpressionBindings extends AbstractMap implements Bindings {
+
+ protected final Object bean;
+
+ protected final Map bindings = new HashMap();
+
+
+ public ExpressionBindings(Object bindingBean) {
+ bean = bindingBean;
+
+ // get method bindings
+ for (Method method : bean.getClass().getMethods()) {
+ Define define = method.getAnnotation(Define.class);
+
+ if (define != null) {
+ for (String name : define.value()) {
+ Method existingBinding = bindings.put(name, method);
+
+ if (existingBinding != null)
+ throw new IllegalArgumentException(String.format("Illegal binding {%s} on %s", name, method.getName()));
+ }
+ }
+ }
+ }
+
+
+ public Object getBindingBean() {
+ return bean;
+ }
+
+
+ protected Object evaluate(Method method) throws Exception {
+ Object value = method.invoke(getBindingBean());
+
+ if (value != null) {
+ return value;
+ }
+
+ // invoke fallback method
+ return bindings.get(Define.undefined).invoke(getBindingBean());
+ }
+
+
+ @Override
+ public Object get(Object key) {
+ Method method = bindings.get(key);
+
+ if (method != null) {
+ try {
+ return evaluate(method);
+ } catch (Exception e) {
+ throw new BindingException(key.toString(), ExceptionUtilities.getRootCauseMessage(e), e);
+ }
+ }
+
+ return null;
+ }
+
+
+ @Override
+ public Object put(String key, Object value) {
+ // bindings are immutable
+ return null;
+ }
+
+
+ @Override
+ public Object remove(Object key) {
+ // bindings are immutable
+ return null;
+ }
+
+
+ @Override
+ public boolean containsKey(Object key) {
+ return bindings.containsKey(key);
+ }
+
+
+ @Override
+ public Set keySet() {
+ return bindings.keySet();
+ }
+
+
+ @Override
+ public boolean isEmpty() {
+ return bindings.isEmpty();
+ }
+
+
+ @Override
+ public Set> entrySet() {
+ Set> entrySet = new HashSet>();
+
+ for (final String key : keySet()) {
+ entrySet.add(new Entry() {
+
+ @Override
+ public String getKey() {
+ return key;
+ }
+
+
+ @Override
+ public Object getValue() {
+ return get(key);
+ }
+
+
+ @Override
+ public Object setValue(Object value) {
+ return put(key, value);
+ }
+ });
+ }
+
+ return entrySet;
+ }
+
+}
diff --git a/source/net/sourceforge/filebot/ui/ExpressionFormat.global.js b/source/net/sourceforge/filebot/format/ExpressionFormat.global.js
similarity index 98%
rename from source/net/sourceforge/filebot/ui/ExpressionFormat.global.js
rename to source/net/sourceforge/filebot/format/ExpressionFormat.global.js
index 8ecaadec..d8eb83ea 100644
--- a/source/net/sourceforge/filebot/ui/ExpressionFormat.global.js
+++ b/source/net/sourceforge/filebot/format/ExpressionFormat.global.js
@@ -8,6 +8,6 @@ String.prototype.pad = Number.prototype.pad = function(length, padding) {
while (s.length < length) {
s = p + s;
}
-
+
return s;
}
diff --git a/source/net/sourceforge/filebot/ui/ExpressionFormat.java b/source/net/sourceforge/filebot/format/ExpressionFormat.java
similarity index 68%
rename from source/net/sourceforge/filebot/ui/ExpressionFormat.java
rename to source/net/sourceforge/filebot/format/ExpressionFormat.java
index 8cc2aa7c..f2a62d4d 100644
--- a/source/net/sourceforge/filebot/ui/ExpressionFormat.java
+++ b/source/net/sourceforge/filebot/format/ExpressionFormat.java
@@ -1,5 +1,5 @@
-package net.sourceforge.filebot.ui;
+package net.sourceforge.filebot.format;
import java.io.InputStreamReader;
@@ -14,17 +14,21 @@ import java.util.regex.Pattern;
import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.CompiledScript;
+import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
+import javax.script.SimpleScriptContext;
-public abstract class ExpressionFormat extends Format {
+public class ExpressionFormat extends Format {
private final String format;
private final Object[] expressions;
+ private ScriptException lastException;
+
public ExpressionFormat(String format) throws ScriptException {
this.format = format;
@@ -35,7 +39,7 @@ public abstract class ExpressionFormat extends Format {
protected ScriptEngine initScriptEngine() throws ScriptException {
ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript");
- engine.eval(new InputStreamReader(getClass().getResourceAsStream("ExpressionFormat.global.js")));
+ engine.eval(new InputStreamReader(ExpressionFormat.class.getResourceAsStream("ExpressionFormat.global.js")));
return engine;
}
@@ -78,33 +82,53 @@ public abstract class ExpressionFormat extends Format {
}
- protected abstract Bindings getBindings(Object value);
+ protected Bindings getBindings(Object value) {
+ // no bindings by default
+ return null;
+ }
@Override
public StringBuffer format(Object object, StringBuffer sb, FieldPosition pos) {
Bindings bindings = getBindings(object);
+ ScriptContext context = new SimpleScriptContext();
+ context.setBindings(bindings, ScriptContext.GLOBAL_SCOPE);
+
try {
for (Object snipped : expressions) {
- if (snipped instanceof String) {
- sb.append(snipped);
- } else {
- Object value = ((CompiledScript) snipped).eval(bindings);
-
- if (value != null) {
- sb.append(value);
+ if (snipped instanceof CompiledScript) {
+ try {
+ Object value = ((CompiledScript) snipped).eval(context);
+
+ if (value != null) {
+ sb.append(value);
+ }
+ } catch (ScriptException e) {
+ lastException = e;
}
+ } else {
+ sb.append(snipped);
}
}
- } catch (ScriptException e) {
- throw new IllegalArgumentException(e);
+ } finally {
+ dispose(bindings);
}
return sb;
}
+ protected void dispose(Bindings bindings) {
+
+ }
+
+
+ public ScriptException scriptException() {
+ return lastException;
+ }
+
+
@Override
public Object parseObject(String source, ParsePosition pos) {
throw new UnsupportedOperationException();
diff --git a/source/net/sourceforge/filebot/ui/EpisodeExpressionFormat.java b/source/net/sourceforge/filebot/ui/EpisodeExpressionFormat.java
deleted file mode 100644
index fcef4455..00000000
--- a/source/net/sourceforge/filebot/ui/EpisodeExpressionFormat.java
+++ /dev/null
@@ -1,38 +0,0 @@
-
-package net.sourceforge.filebot.ui;
-
-
-import javax.script.Bindings;
-import javax.script.ScriptException;
-import javax.script.SimpleBindings;
-
-import net.sourceforge.filebot.web.Episode;
-
-
-public class EpisodeExpressionFormat extends ExpressionFormat {
-
- public EpisodeExpressionFormat(String format) throws ScriptException {
- super(format);
- }
-
-
- @Override
- public Bindings getBindings(Object value) {
- Episode episode = (Episode) value;
-
- Bindings bindings = new SimpleBindings();
-
- bindings.put("n", nonNull(episode.getSeriesName()));
- bindings.put("s", nonNull(episode.getSeasonNumber()));
- bindings.put("e", nonNull(episode.getEpisodeNumber()));
- bindings.put("t", nonNull(episode.getTitle()));
-
- return bindings;
- }
-
-
- private String nonNull(String value) {
- return value == null ? "" : value;
- }
-
-}
diff --git a/source/net/sourceforge/filebot/ui/EpisodeFormatDialog.java b/source/net/sourceforge/filebot/ui/EpisodeFormatDialog.java
index 226a51ec..41957510 100644
--- a/source/net/sourceforge/filebot/ui/EpisodeFormatDialog.java
+++ b/source/net/sourceforge/filebot/ui/EpisodeFormatDialog.java
@@ -11,44 +11,51 @@ import java.awt.Component;
import java.awt.Font;
import java.awt.Window;
import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
+import java.io.File;
import java.text.Format;
-import java.text.ParseException;
import java.util.Arrays;
import java.util.ResourceBundle;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.ThreadPoolExecutor.DiscardOldestPolicy;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.script.ScriptException;
import javax.swing.AbstractAction;
import javax.swing.Action;
-import javax.swing.BorderFactory;
-import javax.swing.InputVerifier;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
-import javax.swing.JFormattedTextField;
+import javax.swing.JFileChooser;
import javax.swing.JLabel;
+import javax.swing.JOptionPane;
import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
-import javax.swing.SwingUtilities;
-import javax.swing.JFormattedTextField.AbstractFormatter;
+import javax.swing.SwingWorker;
+import javax.swing.Timer;
import javax.swing.border.LineBorder;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
-import javax.swing.text.DefaultFormatterFactory;
+import javax.swing.filechooser.FileNameExtensionFilter;
import net.miginfocom.swing.MigLayout;
import net.sourceforge.filebot.ResourceManager;
import net.sourceforge.filebot.Settings;
+import net.sourceforge.filebot.format.EpisodeExpressionFormat;
+import net.sourceforge.filebot.similarity.Match;
import net.sourceforge.filebot.web.Episode;
import net.sourceforge.filebot.web.Episode.EpisodeFormat;
import net.sourceforge.tuned.ExceptionUtilities;
-import net.sourceforge.tuned.PreferencesMap.PreferencesEntry;
import net.sourceforge.tuned.ui.GradientStyle;
import net.sourceforge.tuned.ui.LinkButton;
import net.sourceforge.tuned.ui.TunedUtilities;
@@ -60,16 +67,20 @@ public class EpisodeFormatDialog extends JDialog {
private Format selectedFormat = null;
- protected final JFormattedTextField preview = new JFormattedTextField();
+ private JLabel preview = new JLabel();
- protected final JLabel errorMessage = new JLabel(ResourceManager.getIcon("dialog.cancel"));
- protected final JTextField editor = new JTextField();
+ private JLabel warningMessage = new JLabel(ResourceManager.getIcon("status.warning"));
+ private JLabel errorMessage = new JLabel(ResourceManager.getIcon("status.error"));
- protected Color defaultColor = preview.getForeground();
- protected Color errorColor = Color.red;
+ private Episode previewSampleEpisode = getPreviewSampleEpisode();
+ private File previewSampleMediaFile = getPreviewSampleMediaFile();
- protected final PreferencesEntry persistentFormat = Settings.userRoot().entry("dialog.format");
- protected final PreferencesEntry persistentSample = Settings.userRoot().entry("dialog.sample");
+ private ExecutorService previewExecutor = createPreviewExecutor();
+
+ private JTextField editor = new JTextField();
+
+ private Color defaultColor = preview.getForeground();
+ private Color errorColor = Color.red;
public EpisodeFormatDialog(Window owner) {
@@ -77,17 +88,9 @@ public class EpisodeFormatDialog extends JDialog {
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
+ editor.setText(Settings.userRoot().get("dialog.format"));
editor.setFont(new Font(MONOSPACED, PLAIN, 14));
- // restore state
- preview.setValue(getPreviewSample());
- editor.setText(persistentFormat.getValue());
-
- preview.setBorder(BorderFactory.createEmptyBorder());
-
- // update preview to current format
- checkEpisodeFormat();
-
// bold title label in header
JLabel title = new JLabel(this.getTitle());
title.setFont(title.getFont().deriveFont(BOLD));
@@ -97,9 +100,13 @@ public class EpisodeFormatDialog extends JDialog {
header.setBackground(Color.white);
header.setBorder(new SeparatorBorder(1, new Color(0xB4B4B4), new Color(0xACACAC), GradientStyle.LEFT_TO_RIGHT, Position.BOTTOM));
+ errorMessage.setVisible(false);
+ warningMessage.setVisible(false);
+
header.add(title, "wrap unrel:push");
- header.add(errorMessage, "gap indent, hidemode 3");
- header.add(preview, "gap indent, hidemode 3, growx");
+ header.add(preview, "gap indent, hidemode 3, wmax 90%");
+ header.add(errorMessage, "gap indent, hidemode 3, newline");
+ header.add(warningMessage, "gap indent, hidemode 3, newline");
JPanel content = new JPanel(new MigLayout("insets dialog, nogrid, fill"));
@@ -121,35 +128,31 @@ public class EpisodeFormatDialog extends JDialog {
pane.add(header, "h 60px, growx, dock north");
pane.add(content, "grow");
- pack();
+ setSize(485, 390);
+
+ header.setComponentPopupMenu(createPreviewSamplePopup());
+
setLocation(TunedUtilities.getPreferredLocation(this));
TunedUtilities.putActionForKeystroke(pane, KeyStroke.getKeyStroke("released ESCAPE"), cancelAction);
+ // update preview to current format
+ checkFormatInBackground();
+
// update format on change
- editor.getDocument().addDocumentListener(new DocumentAdapter() {
+ editor.getDocument().addDocumentListener(new LazyDocumentAdapter() {
@Override
- public void update(DocumentEvent evt) {
- checkEpisodeFormat();
+ public void update() {
+ checkFormatInBackground();
}
});
- // keep focus on preview, if current text doesn't fit episode format
- preview.setInputVerifier(new InputVerifier() {
+ addPropertyChangeListener("previewSample", new PropertyChangeListener() {
@Override
- public boolean verify(JComponent input) {
- return checkPreviewSample();
- }
- });
-
- // check edit format on change
- preview.getDocument().addDocumentListener(new DocumentAdapter() {
-
- @Override
- public void update(DocumentEvent evt) {
- checkPreviewSample();
+ public void propertyChange(PropertyChangeEvent evt) {
+ checkFormatInBackground();
}
});
@@ -164,7 +167,52 @@ public class EpisodeFormatDialog extends JDialog {
}
- protected JPanel createSyntaxPanel() {
+ private JPopupMenu createPreviewSamplePopup() {
+ JPopupMenu actionPopup = new JPopupMenu("Sample");
+
+ actionPopup.add(new AbstractAction("Change Episode") {
+
+ @Override
+ public void actionPerformed(ActionEvent evt) {
+ String episode = JOptionPane.showInputDialog(EpisodeFormatDialog.this, null, previewSampleEpisode);
+
+ if (episode != null) {
+ try {
+ previewSampleEpisode = EpisodeFormat.getInstance().parseObject(episode);
+ Settings.userRoot().put("dialog.sample.episode", episode);
+
+ EpisodeFormatDialog.this.firePropertyChange("previewSample", null, previewSample());
+ } catch (Exception e) {
+ Logger.getLogger("ui").warning(String.format("Cannot parse %s", episode));
+ }
+ }
+ }
+ });
+
+ actionPopup.add(new AbstractAction("Change Media File") {
+
+ @Override
+ public void actionPerformed(ActionEvent evt) {
+ JFileChooser fileChooser = new JFileChooser();
+ fileChooser.setSelectedFile(previewSampleMediaFile);
+ fileChooser.setFileFilter(new FileNameExtensionFilter("Media files", "avi", "mkv", "mp4", "ogm"));
+
+ if (fileChooser.showOpenDialog(EpisodeFormatDialog.this) == JFileChooser.APPROVE_OPTION) {
+ previewSampleMediaFile = fileChooser.getSelectedFile();
+ Settings.userRoot().put("dialog.sample.file", previewSampleMediaFile.getAbsolutePath());
+
+ EpisodeFormatDialog.this.firePropertyChange("previewSample", null, previewSample());
+
+ MediaInfoComponent.showDialog(EpisodeFormatDialog.this, previewSampleMediaFile);
+ }
+ }
+ });
+
+ return actionPopup;
+ }
+
+
+ private JPanel createSyntaxPanel() {
JPanel panel = new JPanel(new MigLayout("fill, nogrid"));
panel.setBorder(new LineBorder(new Color(0xACA899)));
@@ -177,7 +225,7 @@ public class EpisodeFormatDialog extends JDialog {
}
- protected JPanel createExamplesPanel() {
+ private JPanel createExamplesPanel() {
JPanel panel = new JPanel(new MigLayout("fill, wrap 3"));
panel.setBorder(new LineBorder(new Color(0xACA899)));
@@ -207,8 +255,13 @@ public class EpisodeFormatDialog extends JDialog {
}
- protected Episode getPreviewSample() {
- String sample = persistentSample.getValue();
+ private Match previewSample() {
+ return new Match(previewSampleEpisode, previewSampleMediaFile);
+ }
+
+
+ private Episode getPreviewSampleEpisode() {
+ String sample = Settings.userRoot().get("dialog.sample.episode");
if (sample != null) {
try {
@@ -223,61 +276,75 @@ public class EpisodeFormatDialog extends JDialog {
}
- protected boolean checkPreviewSample() {
- // check if field is being edited
- if (preview.hasFocus()) {
+ private File getPreviewSampleMediaFile() {
+ String sample = Settings.userRoot().get("dialog.sample.file");
+
+ if (sample != null) {
try {
- // try to parse text
- preview.getFormatter().stringToValue(preview.getText());
+ return new File(sample);
} catch (Exception e) {
- preview.setForeground(errorColor);
- // failed to parse text
- return false;
+ Logger.getLogger(getClass().getName()).log(Level.WARNING, e.getMessage(), e);
}
}
- preview.setForeground(defaultColor);
- return true;
+ // default sample
+ return null;
}
- protected DefaultFormatterFactory createFormatterFactory(Format display) {
- DefaultFormatterFactory factory = new DefaultFormatterFactory();
+ private ExecutorService createPreviewExecutor() {
+ ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.SECONDS, new ArrayBlockingQueue(1));
- factory.setEditFormatter(new SimpleFormatter(EpisodeFormat.getInstance()));
+ // only keep the latest task in the queue
+ executor.setRejectedExecutionHandler(new DiscardOldestPolicy());
- if (display != null) {
- factory.setDisplayFormatter(new SimpleFormatter(display));
- }
-
- return factory;
+ return executor;
}
- protected boolean checkEpisodeFormat() {
- Exception exception = null;
-
- try {
- Format format = new EpisodeExpressionFormat(editor.getText().trim());
+ private void checkFormatInBackground() {
+ previewExecutor.execute(new SwingWorker() {
- // check if format produces empty strings
- if (format.format(preview.getValue()).trim().isEmpty()) {
- throw new IllegalArgumentException("Format must not be empty.");
+ private ScriptException warning = null;
+
+
+ @Override
+ protected String doInBackground() throws Exception {
+ EpisodeExpressionFormat format = new EpisodeExpressionFormat(editor.getText().trim());
+
+ String text = format.format(previewSample());
+ warning = format.scriptException();
+
+ // check if format produces empty strings
+ if (text.trim().isEmpty()) {
+ throw new IllegalArgumentException("Format must not be empty.");
+ }
+
+ return text;
}
- // update preview
- preview.setFormatterFactory(createFormatterFactory(format));
- } catch (Exception e) {
- exception = e;
- }
-
- errorMessage.setText(exception != null ? ExceptionUtilities.getRootCauseMessage(exception) : null);
- errorMessage.setVisible(exception != null);
-
- preview.setVisible(exception == null);
- editor.setForeground(exception == null ? defaultColor : errorColor);
-
- return exception == null;
+
+ @Override
+ protected void done() {
+ Exception error = null;
+
+ try {
+ preview.setText(get());
+ } catch (Exception e) {
+ error = e;
+ }
+
+ errorMessage.setText(error != null ? error.getCause().getMessage() : null);
+ errorMessage.setVisible(error != null);
+
+ warningMessage.setText(warning != null ? warning.getCause().getMessage() : null);
+ warningMessage.setVisible(warning != null);
+
+ preview.setVisible(error == null);
+ editor.setForeground(error == null ? defaultColor : errorColor);
+ }
+
+ });
}
@@ -291,14 +358,6 @@ public class EpisodeFormatDialog extends JDialog {
setVisible(false);
dispose();
-
- if (checkEpisodeFormat()) {
- persistentFormat.setValue(editor.getText());
- }
-
- if (checkPreviewSample()) {
- persistentSample.setValue(EpisodeFormat.getInstance().format(preview.getValue()));
- }
}
protected final Action cancelAction = new AbstractAction("Cancel", ResourceManager.getIcon("dialog.cancel")) {
@@ -323,6 +382,7 @@ public class EpisodeFormatDialog extends JDialog {
public void actionPerformed(ActionEvent evt) {
try {
finish(new EpisodeExpressionFormat(editor.getText()));
+ Settings.userRoot().put("dialog.format", editor.getText());
} catch (ScriptException e) {
Logger.getLogger("ui").log(Level.WARNING, ExceptionUtilities.getRootCauseMessage(e), e);
}
@@ -331,7 +391,7 @@ public class EpisodeFormatDialog extends JDialog {
public static Format showDialog(Component parent) {
- EpisodeFormatDialog dialog = new EpisodeFormatDialog(parent != null ? SwingUtilities.getWindowAncestor(parent) : null);
+ EpisodeFormatDialog dialog = new EpisodeFormatDialog(TunedUtilities.getWindow(parent));
dialog.setVisible(true);
@@ -362,10 +422,10 @@ public class EpisodeFormatDialog extends JDialog {
this.format = format;
// initialize text
- updateText(preview.getValue());
+ updateText(previewSample());
// bind text to preview
- preview.addPropertyChangeListener("value", new PropertyChangeListener() {
+ EpisodeFormatDialog.this.addPropertyChangeListener("previewSample", new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
@@ -387,53 +447,41 @@ public class EpisodeFormatDialog extends JDialog {
}
- protected static class SimpleFormatter extends AbstractFormatter {
+ protected static abstract class LazyDocumentAdapter implements DocumentListener {
- private final Format format;
+ private final Timer timer = new Timer(200, new ActionListener() {
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ update();
+ }
+ });
- public SimpleFormatter(Format format) {
- this.format = format;
+ public LazyDocumentAdapter() {
+ timer.setRepeats(false);
}
- @Override
- public String valueToString(Object value) throws ParseException {
- return format.format(value);
- }
-
-
- @Override
- public Object stringToValue(String text) throws ParseException {
- return format.parseObject(text);
- }
-
- }
-
-
- protected static class DocumentAdapter implements DocumentListener {
-
@Override
public void changedUpdate(DocumentEvent e) {
- update(e);
+ timer.restart();
}
@Override
public void insertUpdate(DocumentEvent e) {
- update(e);
+ timer.restart();
}
@Override
public void removeUpdate(DocumentEvent e) {
- update(e);
+ timer.restart();
}
- public void update(DocumentEvent e) {
-
- }
+ public abstract void update();
}
diff --git a/source/net/sourceforge/filebot/ui/EpisodeFormatDialog.properties b/source/net/sourceforge/filebot/ui/EpisodeFormatDialog.properties
index 6a0fbaa3..8fd964af 100644
--- a/source/net/sourceforge/filebot/ui/EpisodeFormatDialog.properties
+++ b/source/net/sourceforge/filebot/ui/EpisodeFormatDialog.properties
@@ -4,10 +4,10 @@ syntax: { } ... expression, n ... name, s ...
example[0]: {n} - {s}.{e} - {t}
# 1x01
-example[1]: {n} - {if (s) s+'x'}{e.pad(2)}
+example[1]: {n} - {s+'x'}{e.pad(2)}
# S01E01
-example[2]: {n} - {if (s) 'S'+s.pad(2)}E{e.pad(2)}
+example[2]: {n} - {'S'+s.pad(2)}E{e.pad(2)}
# uglyfy name
example[3]: {n.replace(/\\s/g,'.').toLowerCase()}
diff --git a/source/net/sourceforge/filebot/ui/MediaInfoComponent.java b/source/net/sourceforge/filebot/ui/MediaInfoComponent.java
new file mode 100644
index 00000000..70fdb2de
--- /dev/null
+++ b/source/net/sourceforge/filebot/ui/MediaInfoComponent.java
@@ -0,0 +1,122 @@
+
+package net.sourceforge.filebot.ui;
+
+
+import java.awt.Component;
+import java.awt.Dialog.ModalityType;
+import java.awt.event.ActionEvent;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.Map.Entry;
+
+import javax.swing.AbstractAction;
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JDialog;
+import javax.swing.JScrollPane;
+import javax.swing.JTabbedPane;
+import javax.swing.JTable;
+import javax.swing.table.AbstractTableModel;
+
+import net.miginfocom.swing.MigLayout;
+import net.sourceforge.filebot.mediainfo.MediaInfo;
+import net.sourceforge.filebot.mediainfo.MediaInfo.StreamKind;
+import net.sourceforge.tuned.ui.TunedUtilities;
+
+
+public class MediaInfoComponent extends JTabbedPane {
+
+ public MediaInfoComponent(Map>> mediaInfo) {
+ insert(mediaInfo);
+ }
+
+
+ public void insert(Map>> mediaInfo) {
+ // create tabs for all streams
+ for (Entry>> entry : mediaInfo.entrySet()) {
+ for (SortedMap parameters : entry.getValue()) {
+ addTab(entry.getKey().toString(), new JScrollPane(new JTable(new ParameterTableModel(parameters))));
+ }
+ }
+ }
+
+
+ public static void showDialog(Component parent, File file) {
+ final JDialog dialog = new JDialog(TunedUtilities.getWindow(parent), "MediaInfo", ModalityType.DOCUMENT_MODAL);
+
+ JComponent c = (JComponent) dialog.getContentPane();
+ c.setLayout(new MigLayout("fill", "[align center]", "[fill][pref!]"));
+
+ MediaInfo mediaInfo = new MediaInfo();
+ mediaInfo.open(file);
+
+ MediaInfoComponent mediaInfoComponent = new MediaInfoComponent(mediaInfo.snapshot());
+
+ mediaInfo.close();
+
+ c.add(mediaInfoComponent, "grow, wrap");
+
+ c.add(new JButton(new AbstractAction("OK") {
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ dialog.setVisible(false);
+ }
+ }), "wmin 80px, hmin 25px");
+
+ dialog.pack();
+ dialog.setVisible(true);
+ }
+
+
+ protected static class ParameterTableModel extends AbstractTableModel {
+
+ private final List> data;
+
+
+ public ParameterTableModel(Map, ?> data) {
+ this.data = new ArrayList>(data.entrySet());
+ }
+
+
+ @Override
+ public int getRowCount() {
+ return data.size();
+ }
+
+
+ @Override
+ public int getColumnCount() {
+ return 2;
+ }
+
+
+ @Override
+ public String getColumnName(int column) {
+ switch (column) {
+ case 0:
+ return "Parameter";
+ case 1:
+ return "Value";
+ }
+
+ return null;
+ }
+
+
+ @Override
+ public Object getValueAt(int row, int column) {
+ switch (column) {
+ case 0:
+ return data.get(row).getKey();
+ case 1:
+ return data.get(row).getValue();
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/source/net/sourceforge/filebot/ui/panel/rename/RenamePanel.java b/source/net/sourceforge/filebot/ui/panel/rename/RenamePanel.java
index c03153b6..79640e6b 100644
--- a/source/net/sourceforge/filebot/ui/panel/rename/RenamePanel.java
+++ b/source/net/sourceforge/filebot/ui/panel/rename/RenamePanel.java
@@ -33,10 +33,10 @@ import javax.swing.SwingUtilities;
import net.miginfocom.swing.MigLayout;
import net.sourceforge.filebot.ResourceManager;
import net.sourceforge.filebot.Settings;
+import net.sourceforge.filebot.format.EpisodeExpressionFormat;
import net.sourceforge.filebot.similarity.Match;
import net.sourceforge.filebot.similarity.NameSimilarityMetric;
import net.sourceforge.filebot.similarity.SimilarityMetric;
-import net.sourceforge.filebot.ui.EpisodeExpressionFormat;
import net.sourceforge.filebot.ui.EpisodeFormatDialog;
import net.sourceforge.filebot.ui.SelectDialog;
import net.sourceforge.filebot.web.AnidbClient;
diff --git a/source/net/sourceforge/filebot/ui/panel/rename/ValidateNamesDialog.java b/source/net/sourceforge/filebot/ui/panel/rename/ValidateNamesDialog.java
index e1bcc515..d910afeb 100644
--- a/source/net/sourceforge/filebot/ui/panel/rename/ValidateNamesDialog.java
+++ b/source/net/sourceforge/filebot/ui/panel/rename/ValidateNamesDialog.java
@@ -23,7 +23,6 @@ import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.KeyStroke;
-import javax.swing.SwingUtilities;
import net.miginfocom.swing.MigLayout;
import net.sourceforge.filebot.ResourceManager;
@@ -196,7 +195,7 @@ class ValidateNamesDialog extends JDialog {
public static boolean showDialog(Component parent, List source) {
- ValidateNamesDialog dialog = new ValidateNamesDialog(parent != null ? SwingUtilities.getWindowAncestor(parent) : null, source);
+ ValidateNamesDialog dialog = new ValidateNamesDialog(TunedUtilities.getWindow(parent), source);
dialog.setVisible(true);
diff --git a/source/net/sourceforge/tuned/ui/TunedUtilities.java b/source/net/sourceforge/tuned/ui/TunedUtilities.java
index 8a479c59..5154e194 100644
--- a/source/net/sourceforge/tuned/ui/TunedUtilities.java
+++ b/source/net/sourceforge/tuned/ui/TunedUtilities.java
@@ -3,6 +3,7 @@ package net.sourceforge.tuned.ui;
import java.awt.Color;
+import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Image;
@@ -56,6 +57,17 @@ public final class TunedUtilities {
}
+ public static Window getWindow(Component component) {
+ if (component == null)
+ return null;
+
+ if (component instanceof Window)
+ return (Window) component;
+
+ return SwingUtilities.getWindowAncestor(component);
+ }
+
+
public static Point getPreferredLocation(JDialog dialog) {
Window owner = dialog.getOwner();
diff --git a/test/net/sourceforge/filebot/ui/ExpressionFormatTest.java b/test/net/sourceforge/filebot/format/ExpressionFormatTest.java
similarity index 97%
rename from test/net/sourceforge/filebot/ui/ExpressionFormatTest.java
rename to test/net/sourceforge/filebot/format/ExpressionFormatTest.java
index d17138b0..b5aa464e 100644
--- a/test/net/sourceforge/filebot/ui/ExpressionFormatTest.java
+++ b/test/net/sourceforge/filebot/format/ExpressionFormatTest.java
@@ -1,5 +1,5 @@
-package net.sourceforge.filebot.ui;
+package net.sourceforge.filebot.format;
import static org.junit.Assert.assertEquals;