1
0
mirror of https://github.com/mitb-archive/filebot synced 2024-12-23 16:28:51 -05:00

++ first class support for Movie naming scheme!!

* improved binding bean to work with both episode and movie objects
* added Movie support to FormatDialog and BindingDialog
* added Movie format support to CLI

+ added binding for video {source} (DVDRip, BluRay, etc)
+ added binding for release {group} (aXXo, etc)
* added simple binding for audio channels {af} (e.g. 6ch)
* added bindings for multi-part movies {pi}{pn}
* added Movie formatter/parser
This commit is contained in:
Reinhard Pointner 2011-09-18 19:08:03 +00:00
parent ff8eed5af2
commit 339cbfd49e
23 changed files with 617 additions and 288 deletions

BIN
fw/dialog.switch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@ -30,7 +30,7 @@ import java.util.Map.Entry;
import net.sourceforge.filebot.MediaTypes; import net.sourceforge.filebot.MediaTypes;
import net.sourceforge.filebot.WebServices; import net.sourceforge.filebot.WebServices;
import net.sourceforge.filebot.format.EpisodeBindingBean; import net.sourceforge.filebot.format.MediaBindingBean;
import net.sourceforge.filebot.format.ExpressionFormat; import net.sourceforge.filebot.format.ExpressionFormat;
import net.sourceforge.filebot.hash.HashType; import net.sourceforge.filebot.hash.HashType;
import net.sourceforge.filebot.hash.VerificationFileReader; import net.sourceforge.filebot.hash.VerificationFileReader;
@ -101,7 +101,7 @@ public class ArgumentProcessor {
if (getMovieIdentificationService(db) != null) { if (getMovieIdentificationService(db) != null) {
// movie mode // movie mode
return renameMovie(files, getMovieIdentificationService(db), locale, query, strict); return renameMovie(files, query, format, getMovieIdentificationService(db), locale, strict);
} }
// auto-determine mode // auto-determine mode
@ -131,7 +131,7 @@ public class ArgumentProcessor {
if (sxe >= (max * 0.65) || cws >= (max * 0.65)) { if (sxe >= (max * 0.65) || cws >= (max * 0.65)) {
return renameSeries(files, query, format, getEpisodeListProviders()[0], locale, strict); // use default episode db return renameSeries(files, query, format, getEpisodeListProviders()[0], locale, strict); // use default episode db
} else { } else {
return renameMovie(files, getMovieIdentificationServices()[0], locale, query, strict); // use default movie db return renameMovie(files, query, format, getMovieIdentificationServices()[0], locale, strict); // use default movie db
} }
} }
@ -174,7 +174,7 @@ public class ArgumentProcessor {
for (Match<File, Episode> match : matches) { for (Match<File, Episode> match : matches) {
File file = match.getValue(); File file = match.getValue();
Episode episode = match.getCandidate(); Episode episode = match.getCandidate();
String newName = (format != null) ? format.format(new EpisodeBindingBean(episode, file)) : EpisodeFormat.SeasonEpisode.format(episode); String newName = (format != null) ? format.format(new MediaBindingBean(episode, file)) : EpisodeFormat.SeasonEpisode.format(episode);
if (isInvalidFileName(newName)) { if (isInvalidFileName(newName)) {
CLILogger.config("Stripping invalid characters from new name: " + newName); CLILogger.config("Stripping invalid characters from new name: " + newName);
@ -189,7 +189,7 @@ public class ArgumentProcessor {
} }
public Set<File> renameMovie(Collection<File> mediaFiles, MovieIdentificationService db, Locale locale, String query, boolean strict) throws Exception { public Set<File> renameMovie(Collection<File> mediaFiles, String query, ExpressionFormat format, MovieIdentificationService db, Locale locale, boolean strict) throws Exception {
CLILogger.config(format("Rename movies using [%s]", db.getName())); CLILogger.config(format("Rename movies using [%s]", db.getName()));
File[] movieFiles = filter(mediaFiles, VIDEO_FILES).toArray(new File[0]); File[] movieFiles = filter(mediaFiles, VIDEO_FILES).toArray(new File[0]);
@ -209,14 +209,16 @@ public class ArgumentProcessor {
for (int i = 0; i < movieFiles.length; i++) { for (int i = 0; i < movieFiles.length; i++) {
if (movieDescriptors[i] != null) { if (movieDescriptors[i] != null) {
String newName = movieDescriptors[i].toString(); MovieDescriptor movie = movieDescriptors[i];
File file = movieFiles[i];
String newName = (format != null) ? format.format(new MediaBindingBean(movie, file)) : movie.toString();
if (isInvalidFileName(newName)) { if (isInvalidFileName(newName)) {
CLILogger.config("Stripping invalid characters from new path: " + newName); CLILogger.config("Stripping invalid characters from new path: " + newName);
newName = validateFileName(newName); newName = validateFileName(newName);
} }
renameMap.put(movieFiles[i], newName + "." + getExtension(movieFiles[i])); renameMap.put(file, newName + "." + getExtension(file));
} else { } else {
CLILogger.warning("No matching movie: " + movieFiles[i]); CLILogger.warning("No matching movie: " + movieFiles[i]);
} }

View File

@ -2,13 +2,21 @@
package net.sourceforge.filebot.format; package net.sourceforge.filebot.format;
import static java.util.Arrays.*;
import static java.util.ResourceBundle.*;
import static java.util.regex.Pattern.*;
import static net.sourceforge.filebot.MediaTypes.*; import static net.sourceforge.filebot.MediaTypes.*;
import static net.sourceforge.filebot.format.Define.*; import static net.sourceforge.filebot.format.Define.*;
import static net.sourceforge.filebot.hash.VerificationUtilities.*; import static net.sourceforge.filebot.hash.VerificationUtilities.*;
import static net.sourceforge.tuned.StringUtilities.*;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Scanner; import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.sf.ehcache.Cache; import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager; import net.sf.ehcache.CacheManager;
@ -16,72 +24,95 @@ import net.sf.ehcache.Element;
import net.sourceforge.filebot.hash.HashType; import net.sourceforge.filebot.hash.HashType;
import net.sourceforge.filebot.mediainfo.MediaInfo; import net.sourceforge.filebot.mediainfo.MediaInfo;
import net.sourceforge.filebot.mediainfo.MediaInfo.StreamKind; import net.sourceforge.filebot.mediainfo.MediaInfo.StreamKind;
import net.sourceforge.filebot.web.CachedResource;
import net.sourceforge.filebot.web.Date; import net.sourceforge.filebot.web.Date;
import net.sourceforge.filebot.web.Episode; import net.sourceforge.filebot.web.Episode;
import net.sourceforge.filebot.web.MovieDescriptor;
import net.sourceforge.filebot.web.MoviePart;
import net.sourceforge.tuned.FileUtilities; import net.sourceforge.tuned.FileUtilities;
public class EpisodeBindingBean { public class MediaBindingBean {
private final Episode episode;
private final Object infoObject;
private final File mediaFile; private final File mediaFile;
private MediaInfo mediaInfo; private MediaInfo mediaInfo;
public EpisodeBindingBean(Episode episode, File mediaFile) { public MediaBindingBean(Object infoObject, File mediaFile) {
this.episode = episode; this.infoObject = infoObject;
this.mediaFile = mediaFile; this.mediaFile = mediaFile;
} }
@Define(undefined) @Define(undefined)
public String undefined() { public <T> T undefined() {
// omit expressions that depend on undefined values // omit expressions that depend on undefined values
throw new RuntimeException("undefined"); throw new RuntimeException("undefined");
} }
@Define("n") @Define("n")
public String getSeriesName() { public String getName() {
return episode.getSeriesName(); if (infoObject instanceof Episode)
return getEpisode().getSeriesName();
if (infoObject instanceof MovieDescriptor)
return getMovie().getName();
return null;
}
@Define("y")
public Integer getYear() {
if (infoObject instanceof Episode)
return getEpisode().airdate().getYear();
if (infoObject instanceof MovieDescriptor)
return getMovie().getYear();
return null;
} }
@Define("s") @Define("s")
public Integer getSeasonNumber() { public Integer getSeasonNumber() {
return episode.getSeason(); return getEpisode().getSeason();
} }
@Define("e") @Define("e")
public Integer getEpisodeNumber() { public Integer getEpisodeNumber() {
return episode.getEpisode(); return getEpisode().getEpisode();
} }
@Define("t") @Define("t")
public String getTitle() { public String getTitle() {
return episode.getTitle(); return getEpisode().getTitle();
} }
@Define("airdate") @Define("airdate")
public Date airdate() { public Date airdate() {
return episode.airdate(); return getEpisode().airdate();
} }
@Define("absolute") @Define("absolute")
public Integer getAbsoluteEpisodeNumber() { public Integer getAbsoluteEpisodeNumber() {
return episode.getAbsolute(); return getEpisode().getAbsolute();
} }
@Define("special") @Define("special")
public Integer getSpecialNumber() { public Integer getSpecialNumber() {
return episode.getSpecial(); return getEpisode().getSpecial();
}
@Define("imdb")
public Integer getImdbId() {
return getMovie().getImdbId();
} }
@ -128,6 +159,18 @@ public class EpisodeBindingBean {
} }
@Define("af")
public String getAudioChannels() {
String channels = getMediaInfo(StreamKind.Audio, 0, "Channel(s)");
if (channels == null)
return null;
// e.g. 6ch
return channels + "ch";
}
@Define("resolution") @Define("resolution")
public String getVideoResolution() { public String getVideoResolution() {
String width = getMediaInfo(StreamKind.Video, 0, "Width"); String width = getMediaInfo(StreamKind.Video, 0, "Width");
@ -183,6 +226,48 @@ public class EpisodeBindingBean {
} }
@Define("source")
public String getVideoSource() {
// use inferred media file
File inferredMediaFile = getInferredMediaFile();
// pattern matching any video source name
Pattern source = compile(getBundle(getClass().getName()).getString("pattern.video.source"), CASE_INSENSITIVE);
// look for video source patterns in media file and it's parent folder
String lastMatch = null;
for (File it : asList(inferredMediaFile.getParentFile(), inferredMediaFile)) {
for (String part : it.getName().split("[^\\p{Alnum}]")) {
if (source.matcher(part).matches()) {
lastMatch = part;
}
}
}
return lastMatch;
}
@Define("group")
public String getReleaseGroup() throws IOException {
// use inferred media file
File inferredMediaFile = getInferredMediaFile();
// pattern matching any release group name enclosed in separators
Pattern groups = compile("(?<!\\p{Alnum})(" + join(releaseGroups.get(), "|") + ")(?!\\p{Alnum})", CASE_INSENSITIVE);
// look for release group names in media file and it's parent folder
String lastMatch = null;
for (File it : asList(inferredMediaFile.getParentFile(), inferredMediaFile)) {
for (Matcher matcher = groups.matcher(it.getName()); matcher.find();) {
lastMatch = matcher.group();
}
}
return lastMatch;
}
@Define("media") @Define("media")
public Object getGeneralMediaInfo() { public Object getGeneralMediaInfo() {
return new AssociativeScriptObject(getMediaInfo().snapshot(StreamKind.General, 0)); return new AssociativeScriptObject(getMediaInfo().snapshot(StreamKind.General, 0));
@ -209,7 +294,13 @@ public class EpisodeBindingBean {
@Define("episode") @Define("episode")
public Episode getEpisode() { public Episode getEpisode() {
return episode; return (Episode) infoObject;
}
@Define("movie")
public MovieDescriptor getMovie() {
return (MovieDescriptor) infoObject;
} }
@ -219,6 +310,29 @@ public class EpisodeBindingBean {
} }
@Define("part")
public MoviePart getMoviePart() {
return (MoviePart) infoObject;
}
@Define("pi")
public Integer getPart() {
return getMoviePart().getPartIndex();
}
@Define("pn")
public Integer getPartCount() {
return getMoviePart().getPartCount();
}
public Object getInfoObject() {
return infoObject;
}
private File getInferredMediaFile() { private File getInferredMediaFile() {
// make sure media file is defined // make sure media file is defined
checkMediaFile(); checkMediaFile();
@ -289,4 +403,15 @@ public class EpisodeBindingBean {
cache.put(new Element(file, hash)); cache.put(new Element(file, hash));
return hash; return hash;
} }
// fetch release group names online and try to update the data once per day
private final CachedResource<String[]> releaseGroups = new CachedResource<String[]>(getBundle(getClass().getName()).getString("url.release-groups"), 24 * 60 * 60 * 1000) {
@Override
public String[] process(ByteBuffer data) {
return compile("\\s").split(Charset.forName("UTF-8").decode(data));
}
};
} }

View File

@ -0,0 +1,5 @@
# source names mostly copied from [http://en.wikipedia.org/wiki/Pirated_movie_release_types]
pattern.video.source: CAMRip|CAM|TS|TELESYNC|PDVD|TS|TELESYNC|PDVD|PPV|PPVRip|Screener|SCR|SCREENER|DVDSCR|DVDSCREENER|BDSCR|R5|R5LINE|DVDRip|DVDR|TVRip|DSR|PDTV|HDTV|DVBRip|DTHRip|VODRip|VODR|BDRip|BRRip|BluRay|BDR
# group names mostly copied from [http://scenelingo.wordpress.com/list-of-scene-release-groups]
url.release-groups: http://filebot.sourceforge.net/data/release-groups.txt

Binary file not shown.

Before

Width:  |  Height:  |  Size: 723 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -13,13 +13,14 @@ import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter; import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent; import java.awt.event.WindowEvent;
import java.io.File; import java.io.File;
import java.text.Format;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.ResourceBundle; import java.util.ResourceBundle;
import java.util.TreeMap;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@ -54,34 +55,28 @@ import javax.swing.table.TableRowSorter;
import net.miginfocom.swing.MigLayout; import net.miginfocom.swing.MigLayout;
import net.sourceforge.filebot.ResourceManager; import net.sourceforge.filebot.ResourceManager;
import net.sourceforge.filebot.format.EpisodeBindingBean;
import net.sourceforge.filebot.format.ExpressionFormat; import net.sourceforge.filebot.format.ExpressionFormat;
import net.sourceforge.filebot.format.MediaBindingBean;
import net.sourceforge.filebot.mediainfo.MediaInfo; import net.sourceforge.filebot.mediainfo.MediaInfo;
import net.sourceforge.filebot.mediainfo.MediaInfo.StreamKind; import net.sourceforge.filebot.mediainfo.MediaInfo.StreamKind;
import net.sourceforge.filebot.web.Episode;
import net.sourceforge.filebot.web.EpisodeFormat;
import net.sourceforge.tuned.DefaultThreadFactory; import net.sourceforge.tuned.DefaultThreadFactory;
import net.sourceforge.tuned.ui.LazyDocumentListener; import net.sourceforge.tuned.ui.LazyDocumentListener;
class EpisodeBindingDialog extends JDialog { class BindingDialog extends JDialog {
private final JTextField episodeTextField = new JTextField(); private final JTextField infoTextField = new JTextField();
private final JTextField mediaFileTextField = new JTextField(); private final JTextField mediaFileTextField = new JTextField();
private final Format infoObjectFormat;
private final BindingTableModel bindingModel = new BindingTableModel(); private final BindingTableModel bindingModel = new BindingTableModel();
private Option selectedOption = Option.CANCEL; private boolean submit = false;
public enum Option { public BindingDialog(Window owner, String title, Format infoObjectFormat) {
APPROVE, super(owner, title, ModalityType.DOCUMENT_MODAL);
CANCEL this.infoObjectFormat = infoObjectFormat;
}
public EpisodeBindingDialog(Window owner) {
super(owner, "Episode Bindings", ModalityType.DOCUMENT_MODAL);
JComponent root = (JComponent) getContentPane(); JComponent root = (JComponent) getContentPane();
root.setLayout(new MigLayout("nogrid, fill, insets dialog")); root.setLayout(new MigLayout("nogrid, fill, insets dialog"));
@ -94,7 +89,7 @@ class EpisodeBindingDialog extends JDialog {
inputPanel.setOpaque(false); inputPanel.setOpaque(false);
inputPanel.add(new JLabel("Episode:"), "wrap 2px"); inputPanel.add(new JLabel("Episode:"), "wrap 2px");
inputPanel.add(episodeTextField, "hmin 20px, growx, wrap paragraph"); inputPanel.add(infoTextField, "hmin 20px, growx, wrap paragraph");
inputPanel.add(new JLabel("Media File:"), "wrap 2px"); inputPanel.add(new JLabel("Media File:"), "wrap 2px");
inputPanel.add(mediaFileTextField, "hmin 20px, growx"); inputPanel.add(mediaFileTextField, "hmin 20px, growx");
@ -119,12 +114,12 @@ class EpisodeBindingDialog extends JDialog {
if (bindingModel.executor.isShutdown()) if (bindingModel.executor.isShutdown())
return; return;
bindingModel.setModel(getSampleExpressions(), new EpisodeBindingBean(getEpisode(), getMediaFile())); bindingModel.setModel(getSampleExpressions(), new MediaBindingBean(getInfoObject(), getMediaFile()));
} }
}; };
// update example bindings on change // update example bindings on change
episodeTextField.getDocument().addDocumentListener(changeListener); infoTextField.getDocument().addDocumentListener(changeListener);
mediaFileTextField.getDocument().addDocumentListener(changeListener); mediaFileTextField.getDocument().addDocumentListener(changeListener);
// disabled by default // disabled by default
@ -156,7 +151,7 @@ class EpisodeBindingDialog extends JDialog {
@Override @Override
public void windowClosing(WindowEvent e) { public void windowClosing(WindowEvent e) {
finish(Option.CANCEL); finish(false);
} }
}); });
@ -213,27 +208,19 @@ class EpisodeBindingDialog extends JDialog {
} }
private Collection<String> getSampleExpressions() { private List<String> getSampleExpressions() {
ResourceBundle bundle = ResourceBundle.getBundle(getClass().getName()); ResourceBundle bundle = ResourceBundle.getBundle(getClass().getName());
TreeMap<String, String> expressions = new TreeMap<String, String>(); return Arrays.asList(bundle.getString("expressions").split(","));
// extract all expression entries and sort by key
for (String key : bundle.keySet()) {
if (key.startsWith("expr"))
expressions.put(key, bundle.getString(key));
}
return expressions.values();
} }
public Option getSelectedOption() { public boolean submit() {
return selectedOption; return submit;
} }
private void finish(Option option) { private void finish(boolean submit) {
this.selectedOption = option; this.submit = submit;
// cancel background evaluators // cancel background evaluators
bindingModel.executor.shutdownNow(); bindingModel.executor.shutdownNow();
@ -243,8 +230,8 @@ class EpisodeBindingDialog extends JDialog {
} }
public void setEpisode(Episode episode) { public void setInfoObject(Object info) {
episodeTextField.setText(episode == null ? "" : EpisodeFormat.SeasonEpisode.format(episode)); infoTextField.setText(info == null ? "" : infoObjectFormat.format(info));
} }
@ -253,9 +240,9 @@ class EpisodeBindingDialog extends JDialog {
} }
public Episode getEpisode() { public Object getInfoObject() {
try { try {
return EpisodeFormat.Default.parseObject(episodeTextField.getText()); return infoObjectFormat.parseObject(infoTextField.getText());
} catch (Exception e) { } catch (Exception e) {
return null; return null;
} }
@ -275,15 +262,15 @@ class EpisodeBindingDialog extends JDialog {
@Override @Override
public void actionPerformed(ActionEvent evt) { public void actionPerformed(ActionEvent evt) {
// check episode and media file // check episode and media file
if (getEpisode() == null) { if (getInfoObject() == null) {
// illegal episode string // illegal episode string
UILogger.warning(String.format("Failed to parse episode: '%s'", episodeTextField.getText())); UILogger.warning(String.format("Failed to parse episode: '%s'", infoTextField.getText()));
} else if (getMediaFile() == null && !mediaFileTextField.getText().isEmpty()) { } else if (getMediaFile() == null && !mediaFileTextField.getText().isEmpty()) {
// illegal file path // illegal file path
UILogger.warning(String.format("Invalid media file: '%s'", mediaFileTextField.getText())); UILogger.warning(String.format("Invalid media file: '%s'", mediaFileTextField.getText()));
} else { } else {
// everything seems to be in order // everything seems to be in order
finish(Option.APPROVE); finish(true);
} }
} }
}; };
@ -292,7 +279,7 @@ class EpisodeBindingDialog extends JDialog {
@Override @Override
public void actionPerformed(ActionEvent evt) { public void actionPerformed(ActionEvent evt) {
finish(Option.CANCEL); finish(true);
} }
}; };
@ -330,7 +317,7 @@ class EpisodeBindingDialog extends JDialog {
// create table tab for each stream // create table tab for each stream
JTabbedPane tabbedPane = new JTabbedPane(); JTabbedPane tabbedPane = new JTabbedPane();
ResourceBundle bundle = ResourceBundle.getBundle(EpisodeBindingDialog.class.getName()); ResourceBundle bundle = ResourceBundle.getBundle(BindingDialog.class.getName());
RowFilter<Object, Object> excludeRowFilter = RowFilter.notFilter(RowFilter.regexFilter(bundle.getString("parameter.exclude"))); RowFilter<Object, Object> excludeRowFilter = RowFilter.notFilter(RowFilter.regexFilter(bundle.getString("parameter.exclude")));
for (StreamKind streamKind : mediaInfo.keySet()) { for (StreamKind streamKind : mediaInfo.keySet()) {
@ -370,7 +357,7 @@ class EpisodeBindingDialog extends JDialog {
c.add(new JButton(closeAction), "wmin 80px, hmin 25px"); c.add(new JButton(closeAction), "wmin 80px, hmin 25px");
dialog.pack(); dialog.pack();
dialog.setLocationRelativeTo(EpisodeBindingDialog.this); dialog.setLocationRelativeTo(BindingDialog.this);
dialog.setVisible(true); dialog.setVisible(true);
} }

View File

@ -0,0 +1,5 @@
# exclude pattern for media info dialog
parameter.exclude: ^StreamKind|Count$
# preview expressions (keys are tagged so they can be sorted alphabetically)
expressions: n,s,e,t,y,airdate,absolute,special,imdb,episode,movie,vc,ac,cf,vf,af,resolution,source,group,crc32,fn,ext,file,part,pi,pn,media.title,media.durationString,media.overallBitRateString,video.codecID,video.frameRate,video.displayAspectRatioString,video.height,video.scanType,audio.format,audio.bitRateString,audio.language,text.codecInfo,text.language

View File

@ -1,49 +0,0 @@
# exclude pattern for media info dialog
parameter.exclude: ^StreamKind|Count$
# preview expressions (keys are tagged so they can be sorted alphabetically)
# episode expressions
expr[a1]: n
expr[a2]: s
expr[a3]: e
expr[a4]: t
expr[a5]: airdate
expr[a6]: absolute
expr[a7]: special
expr[a8]: episode
# simple mediainfo expressions
expr[b1]: vc
expr[b2]: ac
expr[b3]: cf
expr[b4]: vf
expr[b5]: resolution
# file expressions
expr[c1]: crc32
expr[c2]: fn
expr[c3]: ext
expr[c4]: file
# media info expressions [media]
expr[d1]: media.title
expr[d2]: media.durationString
expr[d3]: media.overallBitRateString
# media info expressions [video]
expr[e1]: video.codecID
expr[e2]: video.frameRate
expr[e3]: video.displayAspectRatioString
expr[e4]: video.height
expr[e5]: video.scanType
# media info expressions [audio]
expr[f1]: audio.format
expr[f2]: audio.channels
expr[f3]: audio.bitRateString
expr[f4]: audio.language
# media info expressions [text]
expr[g1]: text.codecInfo
expr[g2]: text.language

View File

@ -1,13 +0,0 @@
syntax: <html><b>{</b> <b>}</b> \u2026 expression, <b>n</b> \u2026 name, <b>s</b> \u2026 season, <b>e</b> \u2026 episode, <b>t</b> \u2026 title</html>
# basic 1.01
example[0]: {n} - {s}.{e} - {t}
# S01E01
example[1]: {n} - {'S'+s.pad(2)}E{e.pad(2)} - {t}
# 1x01
example[2]: {n} - {s+'x'}{e.pad(2)}
# uglyfy name
example[3]: {n.space('.').lower()}.{s}{e.pad(2)}

View File

@ -5,51 +5,52 @@ package net.sourceforge.filebot.ui.panel.rename;
import static net.sourceforge.tuned.FileUtilities.*; import static net.sourceforge.tuned.FileUtilities.*;
import java.io.File; import java.io.File;
import java.text.Format;
import javax.script.Bindings; import javax.script.Bindings;
import javax.script.ScriptException; import javax.script.ScriptException;
import net.sourceforge.filebot.format.EpisodeBindingBean;
import net.sourceforge.filebot.format.ExpressionBindings; import net.sourceforge.filebot.format.ExpressionBindings;
import net.sourceforge.filebot.format.ExpressionFormat; import net.sourceforge.filebot.format.ExpressionFormat;
import net.sourceforge.filebot.format.MediaBindingBean;
import net.sourceforge.filebot.similarity.Match; import net.sourceforge.filebot.similarity.Match;
import net.sourceforge.filebot.web.Episode;
import net.sourceforge.filebot.web.EpisodeFormat;
class EpisodeExpressionFormatter implements MatchFormatter { class ExpressionFormatter implements MatchFormatter {
private final String expression; private final String expression;
private ExpressionFormat format; private ExpressionFormat format;
private Format preview;
private Class<?> target;
public EpisodeExpressionFormatter(String expression) {
public ExpressionFormatter(String expression, Format preview, Class<?> target) {
if (expression == null || expression.isEmpty()) if (expression == null || expression.isEmpty())
throw new IllegalArgumentException("Expression must not be null or empty"); throw new IllegalArgumentException("Expression must not be null or empty");
this.expression = expression; this.expression = expression;
this.preview = preview;
this.target = target;
} }
@Override @Override
public boolean canFormat(Match<?, ?> match) { public boolean canFormat(Match<?, ?> match) {
// episode is required, file is optional // target object is required, file is optional
return match.getValue() instanceof Episode && (match.getCandidate() == null || match.getCandidate() instanceof File); return target.isInstance(match.getValue()) && (match.getCandidate() == null || match.getCandidate() instanceof File);
} }
@Override @Override
public String preview(Match<?, ?> match) { public String preview(Match<?, ?> match) {
return EpisodeFormat.SeasonEpisode.format(match.getValue()); return preview != null ? preview.format(match.getValue()) : match.getValue().toString();
} }
@Override @Override
public synchronized String format(Match<?, ?> match) throws ScriptException { public synchronized String format(Match<?, ?> match) throws ScriptException {
Episode episode = (Episode) match.getValue();
File mediaFile = (File) match.getCandidate();
// lazy initialize script engine // lazy initialize script engine
if (format == null) { if (format == null) {
format = new ExpressionFormat(expression) { format = new ExpressionFormat(expression) {
@ -76,7 +77,8 @@ class EpisodeExpressionFormatter implements MatchFormatter {
} }
// evaluate the expression using the given bindings // evaluate the expression using the given bindings
String result = format.format(new EpisodeBindingBean(episode, mediaFile)).trim(); Object bindingBean = new MediaBindingBean(match.getValue(), (File) match.getCandidate());
String result = format.format(bindingBean).trim();
// if result is empty, check for script exceptions // if result is empty, check for script exceptions
if (result.isEmpty() && format.caughtScriptException() != null) { if (result.isEmpty() && format.caughtScriptException() != null) {

View File

@ -5,6 +5,7 @@ package net.sourceforge.filebot.ui.panel.rename;
import static java.awt.Font.*; import static java.awt.Font.*;
import static javax.swing.BorderFactory.*; import static javax.swing.BorderFactory.*;
import static net.sourceforge.filebot.ui.NotificationLogging.*; import static net.sourceforge.filebot.ui.NotificationLogging.*;
import static net.sourceforge.tuned.ExceptionUtilities.*;
import static net.sourceforge.tuned.ui.TunedUtilities.*; import static net.sourceforge.tuned.ui.TunedUtilities.*;
import java.awt.Color; import java.awt.Color;
@ -17,6 +18,8 @@ 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.io.File;
import java.text.Format;
import java.text.ParseException;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.ResourceBundle; import java.util.ResourceBundle;
@ -53,11 +56,11 @@ import javax.swing.text.JTextComponent;
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.EpisodeBindingBean; import net.sourceforge.filebot.format.BindingException;
import net.sourceforge.filebot.format.ExpressionFormat; import net.sourceforge.filebot.format.ExpressionFormat;
import net.sourceforge.filebot.web.Date; import net.sourceforge.filebot.format.MediaBindingBean;
import net.sourceforge.filebot.web.Episode;
import net.sourceforge.filebot.web.EpisodeFormat; import net.sourceforge.filebot.web.EpisodeFormat;
import net.sourceforge.filebot.web.MovieFormat;
import net.sourceforge.tuned.DefaultThreadFactory; import net.sourceforge.tuned.DefaultThreadFactory;
import net.sourceforge.tuned.ExceptionUtilities; import net.sourceforge.tuned.ExceptionUtilities;
import net.sourceforge.tuned.PreferencesList; import net.sourceforge.tuned.PreferencesList;
@ -71,49 +74,77 @@ import net.sourceforge.tuned.ui.notification.SeparatorBorder;
import net.sourceforge.tuned.ui.notification.SeparatorBorder.Position; import net.sourceforge.tuned.ui.notification.SeparatorBorder.Position;
class EpisodeFormatDialog extends JDialog { class FormatDialog extends JDialog {
private Option selectedOption = Option.CANCEL; private boolean submit = false;
private ExpressionFormat selectedFormat; private Mode mode;
private ExpressionFormat format;
private EpisodeBindingBean sample = restoreSample();
private MediaBindingBean sample;
private ExecutorService executor = createExecutor(); private ExecutorService executor = createExecutor();
private RunnableFuture<String> currentPreviewFuture; private RunnableFuture<String> currentPreviewFuture;
private JLabel preview = new JLabel(); private JLabel preview = new JLabel();
private JLabel status = new JLabel(); private JLabel status = new JLabel();
private JTextComponent editor = createEditor();
private ProgressIndicator progressIndicator = new ProgressIndicator(); private ProgressIndicator progressIndicator = new ProgressIndicator();
private JTextComponent editor = createEditor(); private JLabel title = new JLabel();
private JPanel help = new JPanel(new MigLayout("insets 0, nogrid, fillx"));
private static final PreferencesEntry<String> persistentSampleEpisode = Settings.forPackage(EpisodeFormatDialog.class).entry("format.sample.episode"); private static final PreferencesEntry<String> persistentSampleFile = Settings.forPackage(FormatDialog.class).entry("format.sample.file");
private static final PreferencesEntry<String> persistentSampleFile = Settings.forPackage(EpisodeFormatDialog.class).entry("format.sample.file");
private static final PreferencesList<String> persistentFormatHistory = Settings.forPackage(EpisodeFormatDialog.class).node("format.recent").asList();
public enum Option { public enum Mode {
APPROVE, Episode,
CANCEL, Movie;
USE_DEFAULT
public Mode next() {
if (ordinal() < values().length - 1)
return values()[ordinal() + 1];
return values()[0];
}
public String key() {
return this.name().toLowerCase();
}
public Format getFormat() {
switch (this) {
case Episode:
return new EpisodeFormat(true, true);
default: // case Movie
return new MovieFormat(true, true, false);
}
}
public PreferencesEntry<String> persistentSample() {
return Settings.forPackage(FormatDialog.class).entry("format.sample." + key());
}
public PreferencesList<String> persistentFormatHistory() {
return Settings.forPackage(FormatDialog.class).node("format.recent." + key()).asList();
}
} }
public EpisodeFormatDialog(Window owner) { public FormatDialog(Window owner) {
super(owner, "Episode Format", ModalityType.DOCUMENT_MODAL); super(owner, ModalityType.DOCUMENT_MODAL);
// initialize hidden // initialize hidden
progressIndicator.setVisible(false); progressIndicator.setVisible(false);
// bold title label in header // bold title label in header
JLabel title = new JLabel(this.getTitle());
title.setFont(title.getFont().deriveFont(BOLD)); title.setFont(title.getFont().deriveFont(BOLD));
JPanel header = new JPanel(new MigLayout("insets dialog, nogrid, fillx")); JPanel header = new JPanel(new MigLayout("insets dialog, nogrid"));
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));
@ -128,13 +159,9 @@ class EpisodeFormatDialog extends JDialog {
content.add(editor, "w 120px:min(pref, 420px), h 40px!, growx, wrap 4px, id editor"); content.add(editor, "w 120px:min(pref, 420px), h 40px!, growx, wrap 4px, id editor");
content.add(createImageButton(changeSampleAction), "w 25!, h 19!, pos n editor.y2+1 editor.x2 n"); content.add(createImageButton(changeSampleAction), "w 25!, h 19!, pos n editor.y2+1 editor.x2 n");
content.add(new JLabel("Syntax"), "gap indent+unrel, wrap 0"); content.add(help, "growx, wrap 25px:push");
content.add(createSyntaxPanel(), "gapx indent indent, wrap 8px");
content.add(new JLabel("Examples"), "gap indent+unrel, wrap 0"); content.add(new JButton(switchEditModeAction), "tag left");
content.add(createExamplesPanel(), "h pref!, gapx indent indent, wrap 25px:push");
content.add(new JButton(useDefaultFormatAction), "tag left");
content.add(new JButton(approveFormatAction), "tag apply"); content.add(new JButton(approveFormatAction), "tag apply");
content.add(new JButton(cancelAction), "tag cancel"); content.add(new JButton(cancelAction), "tag cancel");
@ -166,19 +193,53 @@ class EpisodeFormatDialog extends JDialog {
@Override @Override
public void windowClosing(WindowEvent e) { public void windowClosing(WindowEvent e) {
finish(Option.CANCEL); finish(false);
} }
}); });
// install editor suggestions popup // install editor suggestions popup
TunedUtilities.installAction(editor, KeyStroke.getKeyStroke("DOWN"), displayRecentFormatHistory); TunedUtilities.installAction(editor, KeyStroke.getKeyStroke("DOWN"), displayRecentFormatHistory);
// update preview to current format // episode mode by default
fireSampleChanged(); setMode(Mode.Episode);
// initialize window properties // initialize window properties
setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
pack(); setSize(520, 400);
}
public void setMode(Mode mode) {
this.mode = mode;
this.setTitle(String.format("%s Format", mode));
title.setText(this.getTitle());
status.setVisible(false);
switchEditModeAction.putValue(Action.NAME, String.format("%s Format", mode.next()));
updateHelpPanel(mode);
// update preview to current format
sample = restoreSample(mode);
// restore editor state
editor.setText(mode.persistentFormatHistory().isEmpty() ? "" : mode.persistentFormatHistory().get(0));
// update examples
fireSampleChanged();
}
private JComponent updateHelpPanel(Mode mode) {
help.removeAll();
help.add(new JLabel("Syntax"), "gap indent+unrel, wrap 0");
help.add(createSyntaxPanel(mode), "gapx indent indent, wrap 8px");
help.add(new JLabel("Examples"), "gap indent+unrel, wrap 0");
help.add(createExamplesPanel(mode), "growx, h pref!, gapx indent indent");
return help;
} }
@ -186,9 +247,6 @@ class EpisodeFormatDialog extends JDialog {
final JTextComponent editor = new JTextField(new ExpressionFormatDocument(), null, 0); final JTextComponent editor = new JTextField(new ExpressionFormatDocument(), null, 0);
editor.setFont(new Font(MONOSPACED, PLAIN, 14)); editor.setFont(new Font(MONOSPACED, PLAIN, 14));
// restore editor state
editor.setText(persistentFormatHistory.isEmpty() ? "" : persistentFormatHistory.get(0));
// enable undo/redo // enable undo/redo
installUndoSupport(editor); installUndoSupport(editor);
@ -220,20 +278,20 @@ class EpisodeFormatDialog extends JDialog {
} }
private JComponent createSyntaxPanel() { private JComponent createSyntaxPanel(Mode mode) {
JPanel panel = new JPanel(new MigLayout("fill, nogrid")); JPanel panel = new JPanel(new MigLayout("fill, nogrid"));
panel.setBorder(createLineBorder(new Color(0xACA899))); panel.setBorder(createLineBorder(new Color(0xACA899)));
panel.setBackground(new Color(0xFFFFE1)); panel.setBackground(new Color(0xFFFFE1));
panel.setOpaque(true); panel.setOpaque(true);
panel.add(new JLabel(ResourceBundle.getBundle(getClass().getName()).getString("syntax"))); panel.add(new JLabel(ResourceBundle.getBundle(getClass().getName()).getString(mode.key() + ".syntax")));
return panel; return panel;
} }
private JComponent createExamplesPanel() { private JComponent createExamplesPanel(Mode mode) {
JPanel panel = new JPanel(new MigLayout("fill, wrap 3")); JPanel panel = new JPanel(new MigLayout("fill, wrap 3"));
panel.setBorder(createLineBorder(new Color(0xACA899))); panel.setBorder(createLineBorder(new Color(0xACA899)));
@ -244,7 +302,7 @@ class EpisodeFormatDialog extends JDialog {
// extract all example entries and sort by key // extract all example entries and sort by key
for (String key : bundle.keySet()) { for (String key : bundle.keySet()) {
if (key.startsWith("example")) if (key.startsWith(mode.key() + ".example"))
examples.put(key, bundle.getString(key)); examples.put(key, bundle.getString(key));
} }
@ -259,18 +317,31 @@ class EpisodeFormatDialog extends JDialog {
formatLink.setFont(new Font(MONOSPACED, PLAIN, 11)); formatLink.setFont(new Font(MONOSPACED, PLAIN, 11));
final JLabel formatExample = new JLabel(); // compute format label in background
final JLabel formatExample = new JLabel("[evaluate]");
// bind text to preview // bind text to preview
addPropertyChangeListener("sample", new PropertyChangeListener() { addPropertyChangeListener("sample", new PropertyChangeListener() {
@Override @Override
public void propertyChange(PropertyChangeEvent evt) { public void propertyChange(PropertyChangeEvent evt) {
try { new SwingWorker<String, Void>() {
formatExample.setText(new ExpressionFormat(format).format(sample));
} catch (Exception e) { @Override
Logger.getLogger(getClass().getName()).log(Level.SEVERE, e.getMessage(), e); protected String doInBackground() throws Exception {
} return new ExpressionFormat(format).format(sample);
}
@Override
protected void done() {
try {
formatExample.setText(get());
} catch (Exception e) {
Logger.getLogger(getClass().getName()).log(Level.SEVERE, e.getMessage(), e);
}
}
}.execute();
} }
}); });
@ -283,26 +354,33 @@ class EpisodeFormatDialog extends JDialog {
} }
private EpisodeBindingBean restoreSample() { private MediaBindingBean restoreSample(Mode mode) {
Episode episode = null; Object info = null;
File mediaFile = null; File media = null;
// restore episode
try { try {
episode = EpisodeFormat.Default.parseObject(persistentSampleEpisode.getValue()); // restore sample from user preferences
String sample = mode.persistentSample().getValue();
info = mode.getFormat().parseObject(sample);
} catch (Exception e) { } catch (Exception e) {
// default sample try {
episode = new Episode("Dark Angel", 3, 1, "Labyrinth", 42, null, new Date(2009, 6, 1)); // restore sample from application properties
ResourceBundle bundle = ResourceBundle.getBundle(getClass().getName());
String sample = bundle.getString(mode.key() + ".sample");
info = mode.getFormat().parseObject(sample);
} catch (ParseException illegalSample) {
throw new RuntimeException(illegalSample); // won't happen
}
} }
// restore media file // restore media file
String path = persistentSampleFile.getValue(); String path = persistentSampleFile.getValue();
if (path != null && !path.isEmpty()) { if (path != null && !path.isEmpty()) {
mediaFile = new File(path); media = new File(path);
} }
return new EpisodeBindingBean(episode, mediaFile); return new MediaBindingBean(info, media);
} }
@ -386,7 +464,8 @@ class EpisodeFormatDialog extends JDialog {
} catch (CancellationException e) { } catch (CancellationException e) {
// ignore, cancelled tasks are obsolete anyway // ignore, cancelled tasks are obsolete anyway
} catch (Exception e) { } catch (Exception e) {
status.setText(ExceptionUtilities.getMessage(e)); Exception cause = findCause(e, BindingException.class);
status.setText(getMessage(cause != null ? cause : e));
status.setIcon(ResourceManager.getIcon("status.warning")); status.setIcon(ResourceManager.getIcon("status.warning"));
status.setVisible(true); status.setVisible(true);
} finally { } finally {
@ -423,18 +502,23 @@ class EpisodeFormatDialog extends JDialog {
} }
public Option getSelectedOption() { public boolean submit() {
return selectedOption; return submit;
} }
public ExpressionFormat getSelectedFormat() { public Mode getMode() {
return selectedFormat; return mode;
} }
private void finish(Option option) { public ExpressionFormat getFormat() {
selectedOption = option; return format;
}
private void finish(boolean submit) {
this.submit = submit;
// force shutdown // force shutdown
executor.shutdownNow(); executor.shutdownNow();
@ -448,24 +532,24 @@ class EpisodeFormatDialog extends JDialog {
@Override @Override
public void actionPerformed(ActionEvent evt) { public void actionPerformed(ActionEvent evt) {
EpisodeBindingDialog dialog = new EpisodeBindingDialog(getWindow(evt.getSource())); BindingDialog dialog = new BindingDialog(getWindow(evt.getSource()), String.format("%s Bindings", mode), mode.getFormat());
dialog.setEpisode(sample.getEpisode()); dialog.setInfoObject(sample.getInfoObject());
dialog.setMediaFile(sample.getMediaFile()); dialog.setMediaFile(sample.getMediaFile());
// open dialog // open dialog
dialog.setLocationRelativeTo((Component) evt.getSource()); dialog.setLocationRelativeTo((Component) evt.getSource());
dialog.setVisible(true); dialog.setVisible(true);
if (dialog.getSelectedOption() == EpisodeBindingDialog.Option.APPROVE) { if (dialog.submit()) {
Episode episode = dialog.getEpisode(); Object info = dialog.getInfoObject();
File file = dialog.getMediaFile(); File file = dialog.getMediaFile();
// change sample // change sample
sample = new EpisodeBindingBean(episode, file); sample = new MediaBindingBean(info, file);
// remember // remember
persistentSampleEpisode.setValue(episode == null ? "" : EpisodeFormat.Default.format(sample.getEpisode())); mode.persistentSample().setValue(info == null ? "" : mode.getFormat().format(info));
persistentSampleFile.setValue(file == null ? "" : sample.getMediaFile().getAbsolutePath()); persistentSampleFile.setValue(file == null ? "" : sample.getMediaFile().getAbsolutePath());
// reevaluate everything // reevaluate everything
@ -480,7 +564,7 @@ class EpisodeFormatDialog extends JDialog {
public void actionPerformed(ActionEvent evt) { public void actionPerformed(ActionEvent evt) {
JPopupMenu popup = new JPopupMenu(); JPopupMenu popup = new JPopupMenu();
for (final String expression : persistentFormatHistory) { for (final String expression : mode.persistentFormatHistory()) {
JMenuItem item = popup.add(new AbstractAction(expression) { JMenuItem item = popup.add(new AbstractAction(expression) {
@Override @Override
@ -501,15 +585,15 @@ class EpisodeFormatDialog extends JDialog {
@Override @Override
public void actionPerformed(ActionEvent e) { public void actionPerformed(ActionEvent e) {
finish(Option.CANCEL); finish(false);
} }
}; };
protected final Action useDefaultFormatAction = new AbstractAction("Default", ResourceManager.getIcon("dialog.default")) { protected final Action switchEditModeAction = new AbstractAction(null, ResourceManager.getIcon("dialog.switch")) {
@Override @Override
public void actionPerformed(ActionEvent e) { public void actionPerformed(ActionEvent e) {
finish(Option.USE_DEFAULT); setMode(mode.next());
} }
}; };
@ -519,23 +603,23 @@ class EpisodeFormatDialog extends JDialog {
public void actionPerformed(ActionEvent evt) { public void actionPerformed(ActionEvent evt) {
try { try {
// check syntax // check syntax
selectedFormat = new ExpressionFormat(editor.getText().trim()); format = new ExpressionFormat(editor.getText().trim());
// create new recent history and ignore duplicates // create new recent history and ignore duplicates
Set<String> recent = new LinkedHashSet<String>(); Set<String> recent = new LinkedHashSet<String>();
// add new format first // add new format first
recent.add(selectedFormat.getExpression()); recent.add(format.getExpression());
// add next 4 most recent formats // add next 4 most recent formats
for (int i = 0, limit = Math.min(4, persistentFormatHistory.size()); i < limit; i++) { for (int i = 0, limit = Math.min(4, mode.persistentFormatHistory().size()); i < limit; i++) {
recent.add(persistentFormatHistory.get(i)); recent.add(mode.persistentFormatHistory().get(i));
} }
// update persistent history // update persistent history
persistentFormatHistory.set(recent); mode.persistentFormatHistory().set(recent);
finish(Option.APPROVE); finish(true);
} catch (ScriptException e) { } catch (ScriptException e) {
UILogger.log(Level.WARNING, ExceptionUtilities.getRootCauseMessage(e)); UILogger.log(Level.WARNING, ExceptionUtilities.getRootCauseMessage(e));
} }

View File

@ -0,0 +1,21 @@
episode.syntax: <html><b>{</b> <b>}</b> \u2026 expression, <b>n</b> \u2026 name, <b>s</b> \u2026 season, <b>e</b> \u2026 episode, <b>t</b> \u2026 title</html>
movie.syntax: <html><b>{</b> <b>}</b> \u2026 expression, <b>n</b> \u2026 name, <b>y</b> \u2026 year</html>
episode.sample: Dark Angel - 3x01 - Labyrinth [2009-06-01]
movie.sample: Avatar (2009) Part 1
# basic 1.01
episode.example[0]: {n} - {s}.{e} - {t}
# S01E01
episode.example[1]: {n} - {'S'+s.pad(2)}E{e.pad(2)} - {t}
# 1x01
episode.example[2]: {n} - {s+'x'}{e.pad(2)} - {t}
# uglyfy name
episode.example[3]: {n.space('.').lower()}.{s}{e.pad(2)}
# simple name/year
movie.example[0]: {n} ({y}){" CD$pi"}
# media info name
movie.example[1]: {n} [{y}] {vf} {af}
# normalized scene name
movie.example[2]: {n.space('.')}.{y}.{source}.{vc}

View File

@ -7,6 +7,7 @@ import static net.sourceforge.tuned.FileUtilities.*;
import java.util.Formatter; import java.util.Formatter;
import net.sourceforge.filebot.similarity.Match; import net.sourceforge.filebot.similarity.Match;
import net.sourceforge.filebot.web.MoviePart;
class MovieFormatter implements MatchFormatter { class MovieFormatter implements MatchFormatter {
@ -29,7 +30,7 @@ class MovieFormatter implements MatchFormatter {
Formatter name = new Formatter(new StringBuilder()); Formatter name = new Formatter(new StringBuilder());
// format as single-file or multi-part movie // format as single-file or multi-part movie
name.format("%s (%d)", video.getMovie().getName(), video.getMovie().getYear()); name.format("%s (%d)", video.getName(), video.getYear());
if (video.getPartCount() > 1) if (video.getPartCount() > 1)
name.format(" CD%d", video.getPartIndex() + 1); name.format(" CD%d", video.getPartIndex() + 1);

View File

@ -34,6 +34,7 @@ import net.sourceforge.filebot.similarity.Match;
import net.sourceforge.filebot.ui.SelectDialog; import net.sourceforge.filebot.ui.SelectDialog;
import net.sourceforge.filebot.web.MovieDescriptor; import net.sourceforge.filebot.web.MovieDescriptor;
import net.sourceforge.filebot.web.MovieIdentificationService; import net.sourceforge.filebot.web.MovieIdentificationService;
import net.sourceforge.filebot.web.MoviePart;
class MovieHashMatcher implements AutoCompleteMatcher { class MovieHashMatcher implements AutoCompleteMatcher {
@ -91,7 +92,12 @@ class MovieHashMatcher implements AutoCompleteMatcher {
// add all movie parts // add all movie parts
for (File file : entry.getValue()) { for (File file : entry.getValue()) {
matches.add(new Match<File, MoviePart>(file, new MoviePart(movie, partIndex++, partCount))); MovieDescriptor part = movie;
if (partCount > 1) {
part = new MoviePart(movie, ++partIndex, partCount);
}
matches.add(new Match<File, MovieDescriptor>(file, part));
} }
} }

View File

@ -1,45 +0,0 @@
package net.sourceforge.filebot.ui.panel.rename;
import net.sourceforge.filebot.web.MovieDescriptor;
class MoviePart {
private final MovieDescriptor movie;
private final int partIndex;
private final int partCount;
public MoviePart(MovieDescriptor movie) {
this(movie, 0, 1);
}
public MoviePart(MovieDescriptor movie, int partIndex, int partCount) {
if (partCount < 1 || partIndex >= partCount)
throw new IllegalArgumentException("Illegal part: " + partIndex + "/" + partCount);
this.movie = movie;
this.partIndex = partIndex;
this.partCount = partCount;
}
public MovieDescriptor getMovie() {
return movie;
}
public int getPartIndex() {
return partIndex;
}
public int getPartCount() {
return partCount;
}
}

View File

@ -44,8 +44,10 @@ import net.sourceforge.filebot.similarity.Match;
import net.sourceforge.filebot.ui.Language; import net.sourceforge.filebot.ui.Language;
import net.sourceforge.filebot.ui.panel.rename.RenameModel.FormattedFuture; import net.sourceforge.filebot.ui.panel.rename.RenameModel.FormattedFuture;
import net.sourceforge.filebot.web.Episode; import net.sourceforge.filebot.web.Episode;
import net.sourceforge.filebot.web.EpisodeFormat;
import net.sourceforge.filebot.web.EpisodeListProvider; import net.sourceforge.filebot.web.EpisodeListProvider;
import net.sourceforge.filebot.web.MovieDescriptor; import net.sourceforge.filebot.web.MovieDescriptor;
import net.sourceforge.filebot.web.MovieFormat;
import net.sourceforge.filebot.web.MovieIdentificationService; import net.sourceforge.filebot.web.MovieIdentificationService;
import net.sourceforge.tuned.ExceptionUtilities; import net.sourceforge.tuned.ExceptionUtilities;
import net.sourceforge.tuned.PreferencesMap.PreferencesEntry; import net.sourceforge.tuned.PreferencesMap.PreferencesEntry;
@ -66,7 +68,8 @@ public class RenamePanel extends JComponent {
protected final RenameAction renameAction = new RenameAction(renameModel); protected final RenameAction renameAction = new RenameAction(renameModel);
private static final PreferencesEntry<String> persistentPreserveExtension = Settings.forPackage(RenamePanel.class).entry("rename.extension.preserve").defaultValue("true"); private static final PreferencesEntry<String> persistentPreserveExtension = Settings.forPackage(RenamePanel.class).entry("rename.extension.preserve").defaultValue("true");
private static final PreferencesEntry<String> persistentFormatExpression = Settings.forPackage(RenamePanel.class).entry("rename.format"); private static final PreferencesEntry<String> persistentEpisodeFormat = Settings.forPackage(RenamePanel.class).entry("rename.format.episode");
private static final PreferencesEntry<String> persistentMovieFormat = Settings.forPackage(RenamePanel.class).entry("rename.format.movie");
private static final PreferencesEntry<String> persistentPreferredLanguage = Settings.forPackage(RenamePanel.class).entry("rename.language").defaultValue("en"); private static final PreferencesEntry<String> persistentPreferredLanguage = Settings.forPackage(RenamePanel.class).entry("rename.language").defaultValue("en");
@ -88,7 +91,14 @@ public class RenamePanel extends JComponent {
try { try {
// restore custom episode formatter // restore custom episode formatter
renameModel.useFormatter(Episode.class, new EpisodeExpressionFormatter(persistentFormatExpression.getValue())); renameModel.useFormatter(Episode.class, new ExpressionFormatter(persistentEpisodeFormat.getValue(), EpisodeFormat.SeasonEpisode, Episode.class));
} catch (Exception e) {
// illegal format, ignore
}
try {
// restore custom movie formatter
renameModel.useFormatter(MovieDescriptor.class, new ExpressionFormatter(persistentMovieFormat.getValue(), MovieFormat.NameYear, MovieDescriptor.class));
} catch (Exception e) { } catch (Exception e) {
// illegal format, ignore // illegal format, ignore
} }
@ -166,19 +176,21 @@ public class RenamePanel extends JComponent {
@Override @Override
public void actionPerformed(ActionEvent evt) { public void actionPerformed(ActionEvent evt) {
EpisodeFormatDialog dialog = new EpisodeFormatDialog(getWindowAncestor(RenamePanel.this)); FormatDialog dialog = new FormatDialog(getWindowAncestor(RenamePanel.this));
dialog.setLocation(getOffsetLocation(dialog.getOwner())); dialog.setLocation(getOffsetLocation(dialog.getOwner()));
dialog.setVisible(true); dialog.setVisible(true);
switch (dialog.getSelectedOption()) { if (dialog.submit()) {
case APPROVE: switch (dialog.getMode()) {
renameModel.useFormatter(Episode.class, new EpisodeExpressionFormatter(dialog.getSelectedFormat().getExpression())); case Episode:
persistentFormatExpression.setValue(dialog.getSelectedFormat().getExpression()); renameModel.useFormatter(Episode.class, new ExpressionFormatter(dialog.getFormat().getExpression(), EpisodeFormat.SeasonEpisode, Episode.class));
break; persistentEpisodeFormat.setValue(dialog.getFormat().getExpression());
case USE_DEFAULT: break;
renameModel.useFormatter(Episode.class, null); case Movie:
persistentFormatExpression.remove(); renameModel.useFormatter(MovieDescriptor.class, new ExpressionFormatter(dialog.getFormat().getExpression(), MovieFormat.NameYear, MovieDescriptor.class));
break; persistentMovieFormat.setValue(dialog.getFormat().getExpression());
break;
}
} }
} }
}); });

View File

@ -0,0 +1,56 @@
package net.sourceforge.filebot.web;
import static net.sourceforge.filebot.web.WebRequest.*;
import java.io.IOException;
import java.io.Serializable;
import java.net.URL;
import java.nio.ByteBuffer;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
public abstract class CachedResource<T extends Serializable> {
private Cache cache;
private String resource;
private long expirationTime;
public CachedResource(String resource, long expirationTime) {
this.cache = CacheManager.getInstance().getCache("web-persistent-datasource");
this.resource = resource;
this.expirationTime = expirationTime;
}
/**
* Convert resource data into usable data
*/
public abstract T process(ByteBuffer data);
@SuppressWarnings("unchecked")
public synchronized T get() throws IOException {
Element element = cache.get(resource);
long lastUpdateTime = (element != null) ? element.getLatestOfCreationAndUpdateTime() : 0;
if (element == null || System.currentTimeMillis() - lastUpdateTime > expirationTime) {
ByteBuffer data = fetchIfModified(new URL(resource), element != null ? lastUpdateTime : 0);
if (data != null) {
element = new Element(resource, process(data));
}
// update cached data and last-updated time
cache.put(element);
}
return (T) element.getValue();
}
}

View File

@ -13,7 +13,6 @@ import java.util.regex.Pattern;
public class EpisodeFormat extends Format { public class EpisodeFormat extends Format {
public static final EpisodeFormat SeasonEpisode = new EpisodeFormat(true, false); public static final EpisodeFormat SeasonEpisode = new EpisodeFormat(true, false);
public static final EpisodeFormat Default = new EpisodeFormat(true, true);
private final boolean includeAirdate; private final boolean includeAirdate;
private final boolean includeSpecial; private final boolean includeSpecial;

View File

@ -7,13 +7,12 @@ import java.util.Arrays;
public class MovieDescriptor extends SearchResult { public class MovieDescriptor extends SearchResult {
private final int year; protected final int year;
private final int imdbId; protected final int imdbId;
public MovieDescriptor(String name, int year, int imdbId) { public MovieDescriptor(String name, int year, int imdbId) {
super(name); super(name);
this.year = year; this.year = year;
this.imdbId = imdbId; this.imdbId = imdbId;
} }
@ -48,9 +47,6 @@ public class MovieDescriptor extends SearchResult {
@Override @Override
public String toString() { public String toString() {
if (year < 0)
return name;
return String.format("%s (%d)", name, year); return String.format("%s (%d)", name, year);
} }

View File

@ -0,0 +1,97 @@
package net.sourceforge.filebot.web;
import java.text.FieldPosition;
import java.text.Format;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MovieFormat extends Format {
public static final MovieFormat NameYear = new MovieFormat(true, true, true);
private final boolean includeYear;
private final boolean includePartIndex;
private final boolean smart;
public MovieFormat(boolean includeYear, boolean includePartIndex, boolean smart) {
this.includeYear = includeYear;
this.includePartIndex = includePartIndex;
this.smart = smart;
}
@Override
public StringBuffer format(Object obj, StringBuffer sb, FieldPosition pos) {
// format episode object, e.g. Avatar (2009), Part 1
MovieDescriptor movie = (MovieDescriptor) obj;
sb.append(movie.getName());
if (includeYear) {
if (!smart || movie.getYear() > 0) {
sb.append(' ').append('(').append(movie.getYear()).append(')');
}
}
if (includePartIndex && movie instanceof MoviePart) {
MoviePart part = (MoviePart) movie;
if (!smart || part.partCount > 1) {
sb.append(", Part ").append(part.partIndex);
}
}
return sb;
}
private final Pattern moviePattern = Pattern.compile("([^\\p{Punct}]+?)[\\p{Punct}\\s]+(\\d{4})(?:[\\p{Punct}\\s]+|$)");
private final Pattern partPattern = Pattern.compile("(?:Part|CD)\\D?(\\d)$", Pattern.CASE_INSENSITIVE);
@Override
public MovieDescriptor parseObject(String source, ParsePosition pos) {
String s = source;
Matcher m;
// extract part information first
int partIndex = -1;
int partCount = -1;
if ((m = partPattern.matcher(s)).find()) {
partIndex = Integer.parseInt(m.group(1));
s = m.replaceFirst("");
}
// parse movie information
if ((m = moviePattern.matcher(s)).matches()) {
String name = m.group(1).trim();
int year = Integer.parseInt(m.group(2));
MovieDescriptor movie = new MovieDescriptor(name, year, -1);
if (partIndex >= 0) {
movie = new MoviePart(movie, partIndex, partCount);
}
// did parse input
pos.setIndex(source.length());
return movie;
}
// failed to parse input
pos.setErrorIndex(0);
return null;
}
@Override
public MovieDescriptor parseObject(String source) throws ParseException {
return (MovieDescriptor) super.parseObject(source);
}
}

View File

@ -0,0 +1,33 @@
package net.sourceforge.filebot.web;
public class MoviePart extends MovieDescriptor {
protected final int partIndex;
protected final int partCount;
public MoviePart(MovieDescriptor movie, int partIndex, int partCount) {
super(movie.name, movie.year, movie.imdbId);
this.partIndex = partIndex;
this.partCount = partCount;
}
public int getPartIndex() {
return partIndex;
}
public int getPartCount() {
return partCount;
}
@Override
public String toString() {
return String.format("%s (%d) [%d]", name, year, partIndex);
}
}

View File

@ -106,13 +106,14 @@ public final class WebRequest {
} }
public static ByteBuffer fetch(URL resource) throws IOException { public static ByteBuffer fetchIfModified(URL resource, long ifModifiedSince) throws IOException {
return fetch(resource, null); return fetch(resource, ifModifiedSince, null);
} }
public static ByteBuffer fetch(URL url, Map<String, String> requestParameters) throws IOException { public static ByteBuffer fetch(URL url, long ifModifiedSince, Map<String, String> requestParameters) throws IOException {
URLConnection connection = url.openConnection(); URLConnection connection = url.openConnection();
connection.setIfModifiedSince(ifModifiedSince);
if (requestParameters != null) { if (requestParameters != null) {
for (Entry<String, String> parameter : requestParameters.entrySet()) { for (Entry<String, String> parameter : requestParameters.entrySet()) {
@ -123,7 +124,7 @@ public final class WebRequest {
int contentLength = connection.getContentLength(); int contentLength = connection.getContentLength();
InputStream in = connection.getInputStream(); InputStream in = connection.getInputStream();
ByteBufferOutputStream buffer = new ByteBufferOutputStream(contentLength >= 0 ? contentLength : 32 * 1024); ByteBufferOutputStream buffer = new ByteBufferOutputStream(contentLength >= 0 ? contentLength : 4 * 1024);
try { try {
// read all // read all
@ -138,6 +139,10 @@ public final class WebRequest {
in.close(); in.close();
} }
// no data, e.g. If-Modified-Since requests
if (contentLength < 0 && buffer.getByteBuffer().remaining() == 0)
return null;
return buffer.getByteBuffer(); return buffer.getByteBuffer();
} }