From 7e6f485882f830ad76abeb0343ff54cd6c3e478d Mon Sep 17 00:00:00 2001 From: Reinhard Pointner Date: Sat, 4 Apr 2009 19:36:12 +0000 Subject: [PATCH] * improved episode format and format creation dialog * differentiate between format errors and format warnings --- source/ehcache.xml | 17 +- .../filebot/format/BindingException.java | 16 + .../sourceforge/filebot/format/Define.java | 19 ++ .../format/EpisodeExpressionFormat.java | 41 +++ .../format/EpisodeFormatBindingBean.java | 214 +++++++++++++ .../filebot/format/ExpressionBindings.java | 137 ++++++++ .../{ui => format}/ExpressionFormat.global.js | 2 +- .../{ui => format}/ExpressionFormat.java | 50 ++- .../filebot/ui/EpisodeExpressionFormat.java | 38 --- .../filebot/ui/EpisodeFormatDialog.java | 294 ++++++++++-------- .../filebot/ui/EpisodeFormatDialog.properties | 4 +- .../filebot/ui/MediaInfoComponent.java | 122 ++++++++ .../filebot/ui/panel/rename/RenamePanel.java | 2 +- .../ui/panel/rename/ValidateNamesDialog.java | 3 +- .../sourceforge/tuned/ui/TunedUtilities.java | 12 + .../{ui => format}/ExpressionFormatTest.java | 2 +- 16 files changed, 790 insertions(+), 183 deletions(-) create mode 100644 source/net/sourceforge/filebot/format/BindingException.java create mode 100644 source/net/sourceforge/filebot/format/Define.java create mode 100644 source/net/sourceforge/filebot/format/EpisodeExpressionFormat.java create mode 100644 source/net/sourceforge/filebot/format/EpisodeFormatBindingBean.java create mode 100644 source/net/sourceforge/filebot/format/ExpressionBindings.java rename source/net/sourceforge/filebot/{ui => format}/ExpressionFormat.global.js (98%) rename source/net/sourceforge/filebot/{ui => format}/ExpressionFormat.java (68%) delete mode 100644 source/net/sourceforge/filebot/ui/EpisodeExpressionFormat.java create mode 100644 source/net/sourceforge/filebot/ui/MediaInfoComponent.java rename test/net/sourceforge/filebot/{ui => format}/ExpressionFormatTest.java (97%) 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;