* improved episode format and format creation dialog
* differentiate between format errors and format warnings
This commit is contained in:
parent
c83d4132ec
commit
7e6f485882
|
@ -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"
|
||||
maxElementsInMemory="120"
|
||||
|
@ -130,7 +130,20 @@
|
|||
timeToIdleSeconds="300"
|
||||
timeToLiveSeconds="300"
|
||||
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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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 = "";
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -8,6 +8,6 @@ String.prototype.pad = Number.prototype.pad = function(length, padding) {
|
|||
while (s.length < length) {
|
||||
s = p + s;
|
||||
}
|
||||
|
||||
|
||||
return s;
|
||||
}
|
|
@ -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();
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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<String> persistentFormat = Settings.userRoot().entry("dialog.format");
|
||||
protected final PreferencesEntry<String> 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<Episode, File> previewSample() {
|
||||
return new Match<Episode, File>(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<Runnable>(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<String, Void>() {
|
||||
|
||||
// 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();
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -4,10 +4,10 @@ syntax: <html><b>{</b> <b>}</b> ... expression, <b>n</b> ... name, <b>s</b> ...
|
|||
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()}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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<String> source) {
|
||||
ValidateNamesDialog dialog = new ValidateNamesDialog(parent != null ? SwingUtilities.getWindowAncestor(parent) : null, source);
|
||||
ValidateNamesDialog dialog = new ValidateNamesDialog(TunedUtilities.getWindow(parent), source);
|
||||
|
||||
dialog.setVisible(true);
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
package net.sourceforge.filebot.ui;
|
||||
package net.sourceforge.filebot.format;
|
||||
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
Loading…
Reference in New Issue