* improved episode format and format creation dialog

* differentiate between format errors and format warnings
This commit is contained in:
Reinhard Pointner 2009-04-04 19:36:12 +00:00
parent c83d4132ec
commit 7e6f485882
16 changed files with 790 additions and 183 deletions

View File

@ -122,7 +122,7 @@
<!-- <!--
Simple memory cache named web. Time to live is 5 min. This cache is used by TheTVDBClient and TVRageClient. Short-lived memory cache for web responses. Time to live is 5 min. This cache is used by TheTVDBClient and TVRageClient.
--> -->
<cache name="web" <cache name="web"
maxElementsInMemory="120" maxElementsInMemory="120"
@ -130,7 +130,20 @@
timeToIdleSeconds="300" timeToIdleSeconds="300"
timeToLiveSeconds="300" timeToLiveSeconds="300"
diskPersistent="false" diskPersistent="false"
memoryStoreEvictionPolicy="FIFO" memoryStoreEvictionPolicy="LRU"
/>
<!--
Simple memory cache for calculated checksums. Time to live is 2 hours. This cache is used in EpisodeFormatBindingBean
-->
<cache name="checksum"
maxElementsInMemory="4200"
eternal="false"
timeToIdleSeconds="7200"
timeToLiveSeconds="7200"
diskPersistent="false"
memoryStoreEvictionPolicy="LRU"
/> />
</ehcache> </ehcache>

View File

@ -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);
}
}

View File

@ -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 = "";
}

View File

@ -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<Episode, File> match = (Match<Episode, File>) 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();
}
}

View File

@ -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<String, String> getVideoInfo() {
return getMediaInfo().snapshot(StreamKind.Video, 0);
}
@Define("audio")
public SortedMap<String, String> getAudioInfo() {
return getMediaInfo().snapshot(StreamKind.Audio, 0);
}
@Define("general")
public SortedMap<String, String> 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;
}
}

View File

@ -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<String, Object> implements Bindings {
protected final Object bean;
protected final Map<String, Method> bindings = new HashMap<String, Method>();
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<String> keySet() {
return bindings.keySet();
}
@Override
public boolean isEmpty() {
return bindings.isEmpty();
}
@Override
public Set<Entry<String, Object>> entrySet() {
Set<Entry<String, Object>> entrySet = new HashSet<Entry<String, Object>>();
for (final String key : keySet()) {
entrySet.add(new Entry<String, Object>() {
@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;
}
}

View File

@ -1,5 +1,5 @@
package net.sourceforge.filebot.ui; package net.sourceforge.filebot.format;
import java.io.InputStreamReader; import java.io.InputStreamReader;
@ -14,17 +14,21 @@ import java.util.regex.Pattern;
import javax.script.Bindings; import javax.script.Bindings;
import javax.script.Compilable; import javax.script.Compilable;
import javax.script.CompiledScript; import javax.script.CompiledScript;
import javax.script.ScriptContext;
import javax.script.ScriptEngine; import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager; import javax.script.ScriptEngineManager;
import javax.script.ScriptException; 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 String format;
private final Object[] expressions; private final Object[] expressions;
private ScriptException lastException;
public ExpressionFormat(String format) throws ScriptException { public ExpressionFormat(String format) throws ScriptException {
this.format = format; this.format = format;
@ -35,7 +39,7 @@ public abstract class ExpressionFormat extends Format {
protected ScriptEngine initScriptEngine() throws ScriptException { protected ScriptEngine initScriptEngine() throws ScriptException {
ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript"); 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; 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 @Override
public StringBuffer format(Object object, StringBuffer sb, FieldPosition pos) { public StringBuffer format(Object object, StringBuffer sb, FieldPosition pos) {
Bindings bindings = getBindings(object); Bindings bindings = getBindings(object);
ScriptContext context = new SimpleScriptContext();
context.setBindings(bindings, ScriptContext.GLOBAL_SCOPE);
try { try {
for (Object snipped : expressions) { for (Object snipped : expressions) {
if (snipped instanceof String) { if (snipped instanceof CompiledScript) {
sb.append(snipped); try {
} else { Object value = ((CompiledScript) snipped).eval(context);
Object value = ((CompiledScript) snipped).eval(bindings);
if (value != null) { if (value != null) {
sb.append(value); sb.append(value);
} }
}
}
} catch (ScriptException e) { } catch (ScriptException e) {
throw new IllegalArgumentException(e); lastException = e;
}
} else {
sb.append(snipped);
}
}
} finally {
dispose(bindings);
} }
return sb; return sb;
} }
protected void dispose(Bindings bindings) {
}
public ScriptException scriptException() {
return lastException;
}
@Override @Override
public Object parseObject(String source, ParsePosition pos) { public Object parseObject(String source, ParsePosition pos) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();

View File

@ -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;
}
}

View File

@ -11,44 +11,51 @@ import java.awt.Component;
import java.awt.Font; import java.awt.Font;
import java.awt.Window; import java.awt.Window;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter; import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent; import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener; import java.beans.PropertyChangeListener;
import java.io.File;
import java.text.Format; import java.text.Format;
import java.text.ParseException;
import java.util.Arrays; import java.util.Arrays;
import java.util.ResourceBundle; 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.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.script.ScriptException; import javax.script.ScriptException;
import javax.swing.AbstractAction; import javax.swing.AbstractAction;
import javax.swing.Action; import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.InputVerifier;
import javax.swing.JButton; import javax.swing.JButton;
import javax.swing.JComponent; import javax.swing.JComponent;
import javax.swing.JDialog; import javax.swing.JDialog;
import javax.swing.JFormattedTextField; import javax.swing.JFileChooser;
import javax.swing.JLabel; import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel; import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JTextField; import javax.swing.JTextField;
import javax.swing.KeyStroke; import javax.swing.KeyStroke;
import javax.swing.SwingUtilities; import javax.swing.SwingWorker;
import javax.swing.JFormattedTextField.AbstractFormatter; import javax.swing.Timer;
import javax.swing.border.LineBorder; import javax.swing.border.LineBorder;
import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener; import javax.swing.event.DocumentListener;
import javax.swing.text.DefaultFormatterFactory; import javax.swing.filechooser.FileNameExtensionFilter;
import net.miginfocom.swing.MigLayout; import net.miginfocom.swing.MigLayout;
import net.sourceforge.filebot.ResourceManager; import net.sourceforge.filebot.ResourceManager;
import net.sourceforge.filebot.Settings; 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;
import net.sourceforge.filebot.web.Episode.EpisodeFormat; import net.sourceforge.filebot.web.Episode.EpisodeFormat;
import net.sourceforge.tuned.ExceptionUtilities; import net.sourceforge.tuned.ExceptionUtilities;
import net.sourceforge.tuned.PreferencesMap.PreferencesEntry;
import net.sourceforge.tuned.ui.GradientStyle; import net.sourceforge.tuned.ui.GradientStyle;
import net.sourceforge.tuned.ui.LinkButton; import net.sourceforge.tuned.ui.LinkButton;
import net.sourceforge.tuned.ui.TunedUtilities; import net.sourceforge.tuned.ui.TunedUtilities;
@ -60,16 +67,20 @@ public class EpisodeFormatDialog extends JDialog {
private Format selectedFormat = null; 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")); private JLabel warningMessage = new JLabel(ResourceManager.getIcon("status.warning"));
protected final JTextField editor = new JTextField(); private JLabel errorMessage = new JLabel(ResourceManager.getIcon("status.error"));
protected Color defaultColor = preview.getForeground(); private Episode previewSampleEpisode = getPreviewSampleEpisode();
protected Color errorColor = Color.red; private File previewSampleMediaFile = getPreviewSampleMediaFile();
protected final PreferencesEntry<String> persistentFormat = Settings.userRoot().entry("dialog.format"); private ExecutorService previewExecutor = createPreviewExecutor();
protected final PreferencesEntry<String> persistentSample = Settings.userRoot().entry("dialog.sample");
private JTextField editor = new JTextField();
private Color defaultColor = preview.getForeground();
private Color errorColor = Color.red;
public EpisodeFormatDialog(Window owner) { public EpisodeFormatDialog(Window owner) {
@ -77,17 +88,9 @@ public class EpisodeFormatDialog extends JDialog {
setDefaultCloseOperation(DISPOSE_ON_CLOSE); setDefaultCloseOperation(DISPOSE_ON_CLOSE);
editor.setText(Settings.userRoot().get("dialog.format"));
editor.setFont(new Font(MONOSPACED, PLAIN, 14)); 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 // bold title label in header
JLabel title = new JLabel(this.getTitle()); JLabel title = new JLabel(this.getTitle());
title.setFont(title.getFont().deriveFont(BOLD)); title.setFont(title.getFont().deriveFont(BOLD));
@ -97,9 +100,13 @@ public class EpisodeFormatDialog extends JDialog {
header.setBackground(Color.white); header.setBackground(Color.white);
header.setBorder(new SeparatorBorder(1, new Color(0xB4B4B4), new Color(0xACACAC), GradientStyle.LEFT_TO_RIGHT, Position.BOTTOM)); 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(title, "wrap unrel:push");
header.add(errorMessage, "gap indent, hidemode 3"); header.add(preview, "gap indent, hidemode 3, wmax 90%");
header.add(preview, "gap indent, hidemode 3, growx"); 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")); 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(header, "h 60px, growx, dock north");
pane.add(content, "grow"); pane.add(content, "grow");
pack(); setSize(485, 390);
header.setComponentPopupMenu(createPreviewSamplePopup());
setLocation(TunedUtilities.getPreferredLocation(this)); setLocation(TunedUtilities.getPreferredLocation(this));
TunedUtilities.putActionForKeystroke(pane, KeyStroke.getKeyStroke("released ESCAPE"), cancelAction); TunedUtilities.putActionForKeystroke(pane, KeyStroke.getKeyStroke("released ESCAPE"), cancelAction);
// update preview to current format
checkFormatInBackground();
// update format on change // update format on change
editor.getDocument().addDocumentListener(new DocumentAdapter() { editor.getDocument().addDocumentListener(new LazyDocumentAdapter() {
@Override @Override
public void update(DocumentEvent evt) { public void update() {
checkEpisodeFormat(); checkFormatInBackground();
} }
}); });
// keep focus on preview, if current text doesn't fit episode format addPropertyChangeListener("previewSample", new PropertyChangeListener() {
preview.setInputVerifier(new InputVerifier() {
@Override @Override
public boolean verify(JComponent input) { public void propertyChange(PropertyChangeEvent evt) {
return checkPreviewSample(); checkFormatInBackground();
}
});
// check edit format on change
preview.getDocument().addDocumentListener(new DocumentAdapter() {
@Override
public void update(DocumentEvent evt) {
checkPreviewSample();
} }
}); });
@ -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")); JPanel panel = new JPanel(new MigLayout("fill, nogrid"));
panel.setBorder(new LineBorder(new Color(0xACA899))); 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")); JPanel panel = new JPanel(new MigLayout("fill, wrap 3"));
panel.setBorder(new LineBorder(new Color(0xACA899))); panel.setBorder(new LineBorder(new Color(0xACA899)));
@ -207,8 +255,13 @@ public class EpisodeFormatDialog extends JDialog {
} }
protected Episode getPreviewSample() { private Match<Episode, File> previewSample() {
String sample = persistentSample.getValue(); return new Match<Episode, File>(previewSampleEpisode, previewSampleMediaFile);
}
private Episode getPreviewSampleEpisode() {
String sample = Settings.userRoot().get("dialog.sample.episode");
if (sample != null) { if (sample != null) {
try { try {
@ -223,61 +276,75 @@ public class EpisodeFormatDialog extends JDialog {
} }
protected boolean checkPreviewSample() { private File getPreviewSampleMediaFile() {
// check if field is being edited String sample = Settings.userRoot().get("dialog.sample.file");
if (preview.hasFocus()) {
if (sample != null) {
try { try {
// try to parse text return new File(sample);
preview.getFormatter().stringToValue(preview.getText());
} catch (Exception e) { } catch (Exception e) {
preview.setForeground(errorColor); Logger.getLogger(getClass().getName()).log(Level.WARNING, e.getMessage(), e);
// failed to parse text
return false;
} }
} }
preview.setForeground(defaultColor); // default sample
return true; return null;
} }
protected DefaultFormatterFactory createFormatterFactory(Format display) { private ExecutorService createPreviewExecutor() {
DefaultFormatterFactory factory = new DefaultFormatterFactory(); ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(1));
factory.setEditFormatter(new SimpleFormatter(EpisodeFormat.getInstance())); // only keep the latest task in the queue
executor.setRejectedExecutionHandler(new DiscardOldestPolicy());
if (display != null) { return executor;
factory.setDisplayFormatter(new SimpleFormatter(display));
}
return factory;
} }
protected boolean checkEpisodeFormat() { private void checkFormatInBackground() {
Exception exception = null; previewExecutor.execute(new SwingWorker<String, Void>() {
try { private ScriptException warning = null;
Format format = new EpisodeExpressionFormat(editor.getText().trim());
@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 // check if format produces empty strings
if (format.format(preview.getValue()).trim().isEmpty()) { if (text.trim().isEmpty()) {
throw new IllegalArgumentException("Format must not be empty."); throw new IllegalArgumentException("Format must not be empty.");
} }
// update preview return text;
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); @Override
editor.setForeground(exception == null ? defaultColor : errorColor); protected void done() {
Exception error = null;
return exception == 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); setVisible(false);
dispose(); 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")) { 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) { public void actionPerformed(ActionEvent evt) {
try { try {
finish(new EpisodeExpressionFormat(editor.getText())); finish(new EpisodeExpressionFormat(editor.getText()));
Settings.userRoot().put("dialog.format", editor.getText());
} catch (ScriptException e) { } catch (ScriptException e) {
Logger.getLogger("ui").log(Level.WARNING, ExceptionUtilities.getRootCauseMessage(e), 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) { 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); dialog.setVisible(true);
@ -362,10 +422,10 @@ public class EpisodeFormatDialog extends JDialog {
this.format = format; this.format = format;
// initialize text // initialize text
updateText(preview.getValue()); updateText(previewSample());
// bind text to preview // bind text to preview
preview.addPropertyChangeListener("value", new PropertyChangeListener() { EpisodeFormatDialog.this.addPropertyChangeListener("previewSample", new PropertyChangeListener() {
@Override @Override
public void propertyChange(PropertyChangeEvent evt) { 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;
public SimpleFormatter(Format format) {
this.format = format;
}
private final Timer timer = new Timer(200, new ActionListener() {
@Override @Override
public String valueToString(Object value) throws ParseException { public void actionPerformed(ActionEvent e) {
return format.format(value); update();
}
});
public LazyDocumentAdapter() {
timer.setRepeats(false);
} }
@Override
public Object stringToValue(String text) throws ParseException {
return format.parseObject(text);
}
}
protected static class DocumentAdapter implements DocumentListener {
@Override @Override
public void changedUpdate(DocumentEvent e) { public void changedUpdate(DocumentEvent e) {
update(e); timer.restart();
} }
@Override @Override
public void insertUpdate(DocumentEvent e) { public void insertUpdate(DocumentEvent e) {
update(e); timer.restart();
} }
@Override @Override
public void removeUpdate(DocumentEvent e) { public void removeUpdate(DocumentEvent e) {
update(e); timer.restart();
} }
public void update(DocumentEvent e) { public abstract void update();
}
} }

View File

@ -4,10 +4,10 @@ syntax: <html><b>{</b> <b>}</b> ... expression, <b>n</b> ... name, <b>s</b> ...
example[0]: {n} - {s}.{e} - {t} example[0]: {n} - {s}.{e} - {t}
# 1x01 # 1x01
example[1]: {n} - {if (s) s+'x'}{e.pad(2)} example[1]: {n} - {s+'x'}{e.pad(2)}
# S01E01 # 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 # uglyfy name
example[3]: {n.replace(/\\s/g,'.').toLowerCase()} example[3]: {n.replace(/\\s/g,'.').toLowerCase()}

View File

@ -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<StreamKind, List<SortedMap<String, String>>> mediaInfo) {
insert(mediaInfo);
}
public void insert(Map<StreamKind, List<SortedMap<String, String>>> mediaInfo) {
// create tabs for all streams
for (Entry<StreamKind, List<SortedMap<String, String>>> entry : mediaInfo.entrySet()) {
for (SortedMap<String, String> 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<Entry<?, ?>> data;
public ParameterTableModel(Map<?, ?> data) {
this.data = new ArrayList<Entry<?, ?>>(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;
}
}
}

View File

@ -33,10 +33,10 @@ import javax.swing.SwingUtilities;
import net.miginfocom.swing.MigLayout; import net.miginfocom.swing.MigLayout;
import net.sourceforge.filebot.ResourceManager; import net.sourceforge.filebot.ResourceManager;
import net.sourceforge.filebot.Settings; import net.sourceforge.filebot.Settings;
import net.sourceforge.filebot.format.EpisodeExpressionFormat;
import net.sourceforge.filebot.similarity.Match; import net.sourceforge.filebot.similarity.Match;
import net.sourceforge.filebot.similarity.NameSimilarityMetric; import net.sourceforge.filebot.similarity.NameSimilarityMetric;
import net.sourceforge.filebot.similarity.SimilarityMetric; import net.sourceforge.filebot.similarity.SimilarityMetric;
import net.sourceforge.filebot.ui.EpisodeExpressionFormat;
import net.sourceforge.filebot.ui.EpisodeFormatDialog; import net.sourceforge.filebot.ui.EpisodeFormatDialog;
import net.sourceforge.filebot.ui.SelectDialog; import net.sourceforge.filebot.ui.SelectDialog;
import net.sourceforge.filebot.web.AnidbClient; import net.sourceforge.filebot.web.AnidbClient;

View File

@ -23,7 +23,6 @@ import javax.swing.JLabel;
import javax.swing.JList; import javax.swing.JList;
import javax.swing.JScrollPane; import javax.swing.JScrollPane;
import javax.swing.KeyStroke; import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import net.miginfocom.swing.MigLayout; import net.miginfocom.swing.MigLayout;
import net.sourceforge.filebot.ResourceManager; import net.sourceforge.filebot.ResourceManager;
@ -196,7 +195,7 @@ class ValidateNamesDialog extends JDialog {
public static boolean showDialog(Component parent, List<String> source) { public static boolean showDialog(Component parent, List<String> source) {
ValidateNamesDialog dialog = new ValidateNamesDialog(parent != null ? SwingUtilities.getWindowAncestor(parent) : null, source); ValidateNamesDialog dialog = new ValidateNamesDialog(TunedUtilities.getWindow(parent), source);
dialog.setVisible(true); dialog.setVisible(true);

View File

@ -3,6 +3,7 @@ package net.sourceforge.tuned.ui;
import java.awt.Color; import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension; import java.awt.Dimension;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import java.awt.Image; 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) { public static Point getPreferredLocation(JDialog dialog) {
Window owner = dialog.getOwner(); Window owner = dialog.getOwner();

View File

@ -1,5 +1,5 @@
package net.sourceforge.filebot.ui; package net.sourceforge.filebot.format;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;