diff --git a/fw/dialog.switch.png b/fw/dialog.switch.png new file mode 100644 index 00000000..cfb41ffb Binary files /dev/null and b/fw/dialog.switch.png differ diff --git a/source/net/sourceforge/filebot/cli/ArgumentProcessor.java b/source/net/sourceforge/filebot/cli/ArgumentProcessor.java index 6d207e18..20078920 100644 --- a/source/net/sourceforge/filebot/cli/ArgumentProcessor.java +++ b/source/net/sourceforge/filebot/cli/ArgumentProcessor.java @@ -30,7 +30,7 @@ import java.util.Map.Entry; import net.sourceforge.filebot.MediaTypes; 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.hash.HashType; import net.sourceforge.filebot.hash.VerificationFileReader; @@ -101,7 +101,7 @@ public class ArgumentProcessor { if (getMovieIdentificationService(db) != null) { // movie mode - return renameMovie(files, getMovieIdentificationService(db), locale, query, strict); + return renameMovie(files, query, format, getMovieIdentificationService(db), locale, strict); } // auto-determine mode @@ -131,7 +131,7 @@ public class ArgumentProcessor { if (sxe >= (max * 0.65) || cws >= (max * 0.65)) { return renameSeries(files, query, format, getEpisodeListProviders()[0], locale, strict); // use default episode db } 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 match : matches) { File file = match.getValue(); 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)) { CLILogger.config("Stripping invalid characters from new name: " + newName); @@ -189,7 +189,7 @@ public class ArgumentProcessor { } - public Set renameMovie(Collection mediaFiles, MovieIdentificationService db, Locale locale, String query, boolean strict) throws Exception { + public Set renameMovie(Collection mediaFiles, String query, ExpressionFormat format, MovieIdentificationService db, Locale locale, boolean strict) throws Exception { CLILogger.config(format("Rename movies using [%s]", db.getName())); 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++) { 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)) { CLILogger.config("Stripping invalid characters from new path: " + newName); newName = validateFileName(newName); } - renameMap.put(movieFiles[i], newName + "." + getExtension(movieFiles[i])); + renameMap.put(file, newName + "." + getExtension(file)); } else { CLILogger.warning("No matching movie: " + movieFiles[i]); } diff --git a/source/net/sourceforge/filebot/format/EpisodeBindingBean.java b/source/net/sourceforge/filebot/format/MediaBindingBean.java similarity index 62% rename from source/net/sourceforge/filebot/format/EpisodeBindingBean.java rename to source/net/sourceforge/filebot/format/MediaBindingBean.java index 2041201e..e60d263f 100644 --- a/source/net/sourceforge/filebot/format/EpisodeBindingBean.java +++ b/source/net/sourceforge/filebot/format/MediaBindingBean.java @@ -2,13 +2,21 @@ 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.format.Define.*; import static net.sourceforge.filebot.hash.VerificationUtilities.*; +import static net.sourceforge.tuned.StringUtilities.*; import java.io.File; import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; import java.util.Scanner; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import net.sf.ehcache.Cache; import net.sf.ehcache.CacheManager; @@ -16,72 +24,95 @@ import net.sf.ehcache.Element; import net.sourceforge.filebot.hash.HashType; import net.sourceforge.filebot.mediainfo.MediaInfo; import net.sourceforge.filebot.mediainfo.MediaInfo.StreamKind; +import net.sourceforge.filebot.web.CachedResource; import net.sourceforge.filebot.web.Date; import net.sourceforge.filebot.web.Episode; +import net.sourceforge.filebot.web.MovieDescriptor; +import net.sourceforge.filebot.web.MoviePart; import net.sourceforge.tuned.FileUtilities; -public class EpisodeBindingBean { - - private final Episode episode; +public class MediaBindingBean { + private final Object infoObject; private final File mediaFile; - private MediaInfo mediaInfo; - public EpisodeBindingBean(Episode episode, File mediaFile) { - this.episode = episode; + public MediaBindingBean(Object infoObject, File mediaFile) { + this.infoObject = infoObject; this.mediaFile = mediaFile; } @Define(undefined) - public String undefined() { + public T undefined() { // omit expressions that depend on undefined values throw new RuntimeException("undefined"); } @Define("n") - public String getSeriesName() { - return episode.getSeriesName(); + public String getName() { + 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") public Integer getSeasonNumber() { - return episode.getSeason(); + return getEpisode().getSeason(); } @Define("e") public Integer getEpisodeNumber() { - return episode.getEpisode(); + return getEpisode().getEpisode(); } @Define("t") public String getTitle() { - return episode.getTitle(); + return getEpisode().getTitle(); } @Define("airdate") public Date airdate() { - return episode.airdate(); + return getEpisode().airdate(); } @Define("absolute") public Integer getAbsoluteEpisodeNumber() { - return episode.getAbsolute(); + return getEpisode().getAbsolute(); } @Define("special") 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") public String getVideoResolution() { 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("(? releaseGroups = new CachedResource(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)); + } + }; + } diff --git a/source/net/sourceforge/filebot/format/MediaBindingBean.properties b/source/net/sourceforge/filebot/format/MediaBindingBean.properties new file mode 100644 index 00000000..a96f60a3 --- /dev/null +++ b/source/net/sourceforge/filebot/format/MediaBindingBean.properties @@ -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 \ No newline at end of file diff --git a/source/net/sourceforge/filebot/resources/dialog.default.png b/source/net/sourceforge/filebot/resources/dialog.default.png deleted file mode 100644 index 16db2211..00000000 Binary files a/source/net/sourceforge/filebot/resources/dialog.default.png and /dev/null differ diff --git a/source/net/sourceforge/filebot/resources/dialog.switch.png b/source/net/sourceforge/filebot/resources/dialog.switch.png new file mode 100644 index 00000000..f2a5e167 Binary files /dev/null and b/source/net/sourceforge/filebot/resources/dialog.switch.png differ diff --git a/source/net/sourceforge/filebot/ui/panel/rename/EpisodeBindingDialog.java b/source/net/sourceforge/filebot/ui/panel/rename/BindingDialog.java similarity index 89% rename from source/net/sourceforge/filebot/ui/panel/rename/EpisodeBindingDialog.java rename to source/net/sourceforge/filebot/ui/panel/rename/BindingDialog.java index 7a3a9659..9fd44c8b 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/EpisodeBindingDialog.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/BindingDialog.java @@ -13,13 +13,14 @@ import java.awt.event.ActionEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.File; +import java.text.Format; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.ResourceBundle; -import java.util.TreeMap; import java.util.Map.Entry; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -54,34 +55,28 @@ import javax.swing.table.TableRowSorter; import net.miginfocom.swing.MigLayout; import net.sourceforge.filebot.ResourceManager; -import net.sourceforge.filebot.format.EpisodeBindingBean; import net.sourceforge.filebot.format.ExpressionFormat; +import net.sourceforge.filebot.format.MediaBindingBean; import net.sourceforge.filebot.mediainfo.MediaInfo; 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.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 Format infoObjectFormat; private final BindingTableModel bindingModel = new BindingTableModel(); - private Option selectedOption = Option.CANCEL; + private boolean submit = false; - public enum Option { - APPROVE, - CANCEL - } - - - public EpisodeBindingDialog(Window owner) { - super(owner, "Episode Bindings", ModalityType.DOCUMENT_MODAL); + public BindingDialog(Window owner, String title, Format infoObjectFormat) { + super(owner, title, ModalityType.DOCUMENT_MODAL); + this.infoObjectFormat = infoObjectFormat; JComponent root = (JComponent) getContentPane(); root.setLayout(new MigLayout("nogrid, fill, insets dialog")); @@ -94,7 +89,7 @@ class EpisodeBindingDialog extends JDialog { inputPanel.setOpaque(false); 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(mediaFileTextField, "hmin 20px, growx"); @@ -119,12 +114,12 @@ class EpisodeBindingDialog extends JDialog { if (bindingModel.executor.isShutdown()) return; - bindingModel.setModel(getSampleExpressions(), new EpisodeBindingBean(getEpisode(), getMediaFile())); + bindingModel.setModel(getSampleExpressions(), new MediaBindingBean(getInfoObject(), getMediaFile())); } }; // update example bindings on change - episodeTextField.getDocument().addDocumentListener(changeListener); + infoTextField.getDocument().addDocumentListener(changeListener); mediaFileTextField.getDocument().addDocumentListener(changeListener); // disabled by default @@ -156,7 +151,7 @@ class EpisodeBindingDialog extends JDialog { @Override public void windowClosing(WindowEvent e) { - finish(Option.CANCEL); + finish(false); } }); @@ -213,27 +208,19 @@ class EpisodeBindingDialog extends JDialog { } - private Collection getSampleExpressions() { + private List getSampleExpressions() { ResourceBundle bundle = ResourceBundle.getBundle(getClass().getName()); - TreeMap expressions = new TreeMap(); - - // 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(); + return Arrays.asList(bundle.getString("expressions").split(",")); } - public Option getSelectedOption() { - return selectedOption; + public boolean submit() { + return submit; } - private void finish(Option option) { - this.selectedOption = option; + private void finish(boolean submit) { + this.submit = submit; // cancel background evaluators bindingModel.executor.shutdownNow(); @@ -243,8 +230,8 @@ class EpisodeBindingDialog extends JDialog { } - public void setEpisode(Episode episode) { - episodeTextField.setText(episode == null ? "" : EpisodeFormat.SeasonEpisode.format(episode)); + public void setInfoObject(Object info) { + infoTextField.setText(info == null ? "" : infoObjectFormat.format(info)); } @@ -253,9 +240,9 @@ class EpisodeBindingDialog extends JDialog { } - public Episode getEpisode() { + public Object getInfoObject() { try { - return EpisodeFormat.Default.parseObject(episodeTextField.getText()); + return infoObjectFormat.parseObject(infoTextField.getText()); } catch (Exception e) { return null; } @@ -275,15 +262,15 @@ class EpisodeBindingDialog extends JDialog { @Override public void actionPerformed(ActionEvent evt) { // check episode and media file - if (getEpisode() == null) { + if (getInfoObject() == null) { // 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()) { // illegal file path UILogger.warning(String.format("Invalid media file: '%s'", mediaFileTextField.getText())); } else { // everything seems to be in order - finish(Option.APPROVE); + finish(true); } } }; @@ -292,7 +279,7 @@ class EpisodeBindingDialog extends JDialog { @Override public void actionPerformed(ActionEvent evt) { - finish(Option.CANCEL); + finish(true); } }; @@ -330,7 +317,7 @@ class EpisodeBindingDialog extends JDialog { // create table tab for each stream JTabbedPane tabbedPane = new JTabbedPane(); - ResourceBundle bundle = ResourceBundle.getBundle(EpisodeBindingDialog.class.getName()); + ResourceBundle bundle = ResourceBundle.getBundle(BindingDialog.class.getName()); RowFilter excludeRowFilter = RowFilter.notFilter(RowFilter.regexFilter(bundle.getString("parameter.exclude"))); for (StreamKind streamKind : mediaInfo.keySet()) { @@ -370,7 +357,7 @@ class EpisodeBindingDialog extends JDialog { c.add(new JButton(closeAction), "wmin 80px, hmin 25px"); dialog.pack(); - dialog.setLocationRelativeTo(EpisodeBindingDialog.this); + dialog.setLocationRelativeTo(BindingDialog.this); dialog.setVisible(true); } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/BindingDialog.properties b/source/net/sourceforge/filebot/ui/panel/rename/BindingDialog.properties new file mode 100644 index 00000000..64f2bc87 --- /dev/null +++ b/source/net/sourceforge/filebot/ui/panel/rename/BindingDialog.properties @@ -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 diff --git a/source/net/sourceforge/filebot/ui/panel/rename/EpisodeBindingDialog.properties b/source/net/sourceforge/filebot/ui/panel/rename/EpisodeBindingDialog.properties deleted file mode 100644 index 805b26c3..00000000 --- a/source/net/sourceforge/filebot/ui/panel/rename/EpisodeBindingDialog.properties +++ /dev/null @@ -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 diff --git a/source/net/sourceforge/filebot/ui/panel/rename/EpisodeFormatDialog.properties b/source/net/sourceforge/filebot/ui/panel/rename/EpisodeFormatDialog.properties deleted file mode 100644 index 8ad6f40b..00000000 --- a/source/net/sourceforge/filebot/ui/panel/rename/EpisodeFormatDialog.properties +++ /dev/null @@ -1,13 +0,0 @@ -syntax: { } \u2026 expression, n \u2026 name, s \u2026 season, e \u2026 episode, t \u2026 title - -# 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)} diff --git a/source/net/sourceforge/filebot/ui/panel/rename/EpisodeExpressionFormatter.java b/source/net/sourceforge/filebot/ui/panel/rename/ExpressionFormatter.java similarity index 72% rename from source/net/sourceforge/filebot/ui/panel/rename/EpisodeExpressionFormatter.java rename to source/net/sourceforge/filebot/ui/panel/rename/ExpressionFormatter.java index acda69a4..fab1cc80 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/EpisodeExpressionFormatter.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/ExpressionFormatter.java @@ -5,51 +5,52 @@ package net.sourceforge.filebot.ui.panel.rename; import static net.sourceforge.tuned.FileUtilities.*; import java.io.File; +import java.text.Format; import javax.script.Bindings; import javax.script.ScriptException; -import net.sourceforge.filebot.format.EpisodeBindingBean; import net.sourceforge.filebot.format.ExpressionBindings; import net.sourceforge.filebot.format.ExpressionFormat; +import net.sourceforge.filebot.format.MediaBindingBean; 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 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()) throw new IllegalArgumentException("Expression must not be null or empty"); this.expression = expression; + this.preview = preview; + this.target = target; + } @Override public boolean canFormat(Match match) { - // episode is required, file is optional - return match.getValue() instanceof Episode && (match.getCandidate() == null || match.getCandidate() instanceof File); + // target object is required, file is optional + return target.isInstance(match.getValue()) && (match.getCandidate() == null || match.getCandidate() instanceof File); } @Override public String preview(Match match) { - return EpisodeFormat.SeasonEpisode.format(match.getValue()); + return preview != null ? preview.format(match.getValue()) : match.getValue().toString(); } @Override public synchronized String format(Match match) throws ScriptException { - Episode episode = (Episode) match.getValue(); - File mediaFile = (File) match.getCandidate(); - // lazy initialize script engine if (format == null) { format = new ExpressionFormat(expression) { @@ -76,7 +77,8 @@ class EpisodeExpressionFormatter implements MatchFormatter { } // 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.isEmpty() && format.caughtScriptException() != null) { diff --git a/source/net/sourceforge/filebot/ui/panel/rename/EpisodeFormatDialog.java b/source/net/sourceforge/filebot/ui/panel/rename/FormatDialog.java similarity index 72% rename from source/net/sourceforge/filebot/ui/panel/rename/EpisodeFormatDialog.java rename to source/net/sourceforge/filebot/ui/panel/rename/FormatDialog.java index 871f8000..90a76770 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/EpisodeFormatDialog.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/FormatDialog.java @@ -5,6 +5,7 @@ package net.sourceforge.filebot.ui.panel.rename; import static java.awt.Font.*; import static javax.swing.BorderFactory.*; import static net.sourceforge.filebot.ui.NotificationLogging.*; +import static net.sourceforge.tuned.ExceptionUtilities.*; import static net.sourceforge.tuned.ui.TunedUtilities.*; import java.awt.Color; @@ -17,6 +18,8 @@ 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.LinkedHashSet; import java.util.List; import java.util.ResourceBundle; @@ -53,11 +56,11 @@ import javax.swing.text.JTextComponent; import net.miginfocom.swing.MigLayout; import net.sourceforge.filebot.ResourceManager; 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.web.Date; -import net.sourceforge.filebot.web.Episode; +import net.sourceforge.filebot.format.MediaBindingBean; import net.sourceforge.filebot.web.EpisodeFormat; +import net.sourceforge.filebot.web.MovieFormat; import net.sourceforge.tuned.DefaultThreadFactory; import net.sourceforge.tuned.ExceptionUtilities; import net.sourceforge.tuned.PreferencesList; @@ -71,49 +74,77 @@ import net.sourceforge.tuned.ui.notification.SeparatorBorder; 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 EpisodeBindingBean sample = restoreSample(); + private Mode mode; + private ExpressionFormat format; + private MediaBindingBean sample; private ExecutorService executor = createExecutor(); - private RunnableFuture currentPreviewFuture; private JLabel preview = new JLabel(); - private JLabel status = new JLabel(); + private JTextComponent editor = createEditor(); 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 persistentSampleEpisode = Settings.forPackage(EpisodeFormatDialog.class).entry("format.sample.episode"); - private static final PreferencesEntry persistentSampleFile = Settings.forPackage(EpisodeFormatDialog.class).entry("format.sample.file"); - private static final PreferencesList persistentFormatHistory = Settings.forPackage(EpisodeFormatDialog.class).node("format.recent").asList(); + private static final PreferencesEntry persistentSampleFile = Settings.forPackage(FormatDialog.class).entry("format.sample.file"); - public enum Option { - APPROVE, - CANCEL, - USE_DEFAULT + public enum Mode { + Episode, + Movie; + + 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 persistentSample() { + return Settings.forPackage(FormatDialog.class).entry("format.sample." + key()); + } + + + public PreferencesList persistentFormatHistory() { + return Settings.forPackage(FormatDialog.class).node("format.recent." + key()).asList(); + } } - public EpisodeFormatDialog(Window owner) { - super(owner, "Episode Format", ModalityType.DOCUMENT_MODAL); + public FormatDialog(Window owner) { + super(owner, ModalityType.DOCUMENT_MODAL); // initialize hidden progressIndicator.setVisible(false); // bold title label in header - JLabel title = new JLabel(this.getTitle()); 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.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(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(createSyntaxPanel(), "gapx indent indent, wrap 8px"); + content.add(help, "growx, wrap 25px:push"); - content.add(new JLabel("Examples"), "gap indent+unrel, wrap 0"); - content.add(createExamplesPanel(), "h pref!, gapx indent indent, wrap 25px:push"); - - content.add(new JButton(useDefaultFormatAction), "tag left"); + content.add(new JButton(switchEditModeAction), "tag left"); content.add(new JButton(approveFormatAction), "tag apply"); content.add(new JButton(cancelAction), "tag cancel"); @@ -166,19 +193,53 @@ class EpisodeFormatDialog extends JDialog { @Override public void windowClosing(WindowEvent e) { - finish(Option.CANCEL); + finish(false); } }); // install editor suggestions popup TunedUtilities.installAction(editor, KeyStroke.getKeyStroke("DOWN"), displayRecentFormatHistory); - // update preview to current format - fireSampleChanged(); + // episode mode by default + setMode(Mode.Episode); // initialize window properties 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); editor.setFont(new Font(MONOSPACED, PLAIN, 14)); - // restore editor state - editor.setText(persistentFormatHistory.isEmpty() ? "" : persistentFormatHistory.get(0)); - // enable undo/redo 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")); panel.setBorder(createLineBorder(new Color(0xACA899))); panel.setBackground(new Color(0xFFFFE1)); 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; } - private JComponent createExamplesPanel() { + private JComponent createExamplesPanel(Mode mode) { JPanel panel = new JPanel(new MigLayout("fill, wrap 3")); panel.setBorder(createLineBorder(new Color(0xACA899))); @@ -244,7 +302,7 @@ class EpisodeFormatDialog extends JDialog { // extract all example entries and sort by key for (String key : bundle.keySet()) { - if (key.startsWith("example")) + if (key.startsWith(mode.key() + ".example")) examples.put(key, bundle.getString(key)); } @@ -259,18 +317,31 @@ class EpisodeFormatDialog extends JDialog { 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 addPropertyChangeListener("sample", new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { - try { - formatExample.setText(new ExpressionFormat(format).format(sample)); - } catch (Exception e) { - Logger.getLogger(getClass().getName()).log(Level.SEVERE, e.getMessage(), e); - } + new SwingWorker() { + + @Override + 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() { - Episode episode = null; - File mediaFile = null; + private MediaBindingBean restoreSample(Mode mode) { + Object info = null; + File media = null; - // restore episode 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) { - // default sample - episode = new Episode("Dark Angel", 3, 1, "Labyrinth", 42, null, new Date(2009, 6, 1)); + try { + // 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 String path = persistentSampleFile.getValue(); 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) { // ignore, cancelled tasks are obsolete anyway } 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.setVisible(true); } finally { @@ -423,18 +502,23 @@ class EpisodeFormatDialog extends JDialog { } - public Option getSelectedOption() { - return selectedOption; + public boolean submit() { + return submit; } - public ExpressionFormat getSelectedFormat() { - return selectedFormat; + public Mode getMode() { + return mode; } - private void finish(Option option) { - selectedOption = option; + public ExpressionFormat getFormat() { + return format; + } + + + private void finish(boolean submit) { + this.submit = submit; // force shutdown executor.shutdownNow(); @@ -448,24 +532,24 @@ class EpisodeFormatDialog extends JDialog { @Override 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()); // open dialog dialog.setLocationRelativeTo((Component) evt.getSource()); dialog.setVisible(true); - if (dialog.getSelectedOption() == EpisodeBindingDialog.Option.APPROVE) { - Episode episode = dialog.getEpisode(); + if (dialog.submit()) { + Object info = dialog.getInfoObject(); File file = dialog.getMediaFile(); // change sample - sample = new EpisodeBindingBean(episode, file); + sample = new MediaBindingBean(info, file); // 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()); // reevaluate everything @@ -480,7 +564,7 @@ class EpisodeFormatDialog extends JDialog { public void actionPerformed(ActionEvent evt) { JPopupMenu popup = new JPopupMenu(); - for (final String expression : persistentFormatHistory) { + for (final String expression : mode.persistentFormatHistory()) { JMenuItem item = popup.add(new AbstractAction(expression) { @Override @@ -501,15 +585,15 @@ class EpisodeFormatDialog extends JDialog { @Override 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 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) { try { // check syntax - selectedFormat = new ExpressionFormat(editor.getText().trim()); + format = new ExpressionFormat(editor.getText().trim()); // create new recent history and ignore duplicates Set recent = new LinkedHashSet(); // add new format first - recent.add(selectedFormat.getExpression()); + recent.add(format.getExpression()); // add next 4 most recent formats - for (int i = 0, limit = Math.min(4, persistentFormatHistory.size()); i < limit; i++) { - recent.add(persistentFormatHistory.get(i)); + for (int i = 0, limit = Math.min(4, mode.persistentFormatHistory().size()); i < limit; i++) { + recent.add(mode.persistentFormatHistory().get(i)); } // update persistent history - persistentFormatHistory.set(recent); + mode.persistentFormatHistory().set(recent); - finish(Option.APPROVE); + finish(true); } catch (ScriptException e) { UILogger.log(Level.WARNING, ExceptionUtilities.getRootCauseMessage(e)); } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/FormatDialog.properties b/source/net/sourceforge/filebot/ui/panel/rename/FormatDialog.properties new file mode 100644 index 00000000..2f28502b --- /dev/null +++ b/source/net/sourceforge/filebot/ui/panel/rename/FormatDialog.properties @@ -0,0 +1,21 @@ +episode.syntax: { } \u2026 expression, n \u2026 name, s \u2026 season, e \u2026 episode, t \u2026 title +movie.syntax: { } \u2026 expression, n \u2026 name, y \u2026 year + +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} diff --git a/source/net/sourceforge/filebot/ui/panel/rename/MovieFormatter.java b/source/net/sourceforge/filebot/ui/panel/rename/MovieFormatter.java index 31649bbc..570f9f27 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/MovieFormatter.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/MovieFormatter.java @@ -7,6 +7,7 @@ import static net.sourceforge.tuned.FileUtilities.*; import java.util.Formatter; import net.sourceforge.filebot.similarity.Match; +import net.sourceforge.filebot.web.MoviePart; class MovieFormatter implements MatchFormatter { @@ -29,7 +30,7 @@ class MovieFormatter implements MatchFormatter { Formatter name = new Formatter(new StringBuilder()); // 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) name.format(" CD%d", video.getPartIndex() + 1); diff --git a/source/net/sourceforge/filebot/ui/panel/rename/MovieHashMatcher.java b/source/net/sourceforge/filebot/ui/panel/rename/MovieHashMatcher.java index b3e1500f..d9bfd9d5 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/MovieHashMatcher.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/MovieHashMatcher.java @@ -34,6 +34,7 @@ import net.sourceforge.filebot.similarity.Match; import net.sourceforge.filebot.ui.SelectDialog; import net.sourceforge.filebot.web.MovieDescriptor; import net.sourceforge.filebot.web.MovieIdentificationService; +import net.sourceforge.filebot.web.MoviePart; class MovieHashMatcher implements AutoCompleteMatcher { @@ -91,7 +92,12 @@ class MovieHashMatcher implements AutoCompleteMatcher { // add all movie parts for (File file : entry.getValue()) { - matches.add(new Match(file, new MoviePart(movie, partIndex++, partCount))); + MovieDescriptor part = movie; + if (partCount > 1) { + part = new MoviePart(movie, ++partIndex, partCount); + } + + matches.add(new Match(file, part)); } } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/MoviePart.java b/source/net/sourceforge/filebot/ui/panel/rename/MoviePart.java deleted file mode 100644 index e4455d5e..00000000 --- a/source/net/sourceforge/filebot/ui/panel/rename/MoviePart.java +++ /dev/null @@ -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; - } - -} diff --git a/source/net/sourceforge/filebot/ui/panel/rename/RenamePanel.java b/source/net/sourceforge/filebot/ui/panel/rename/RenamePanel.java index ba1559f5..f1f4edfb 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/RenamePanel.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/RenamePanel.java @@ -44,8 +44,10 @@ import net.sourceforge.filebot.similarity.Match; import net.sourceforge.filebot.ui.Language; import net.sourceforge.filebot.ui.panel.rename.RenameModel.FormattedFuture; import net.sourceforge.filebot.web.Episode; +import net.sourceforge.filebot.web.EpisodeFormat; import net.sourceforge.filebot.web.EpisodeListProvider; import net.sourceforge.filebot.web.MovieDescriptor; +import net.sourceforge.filebot.web.MovieFormat; import net.sourceforge.filebot.web.MovieIdentificationService; import net.sourceforge.tuned.ExceptionUtilities; import net.sourceforge.tuned.PreferencesMap.PreferencesEntry; @@ -66,7 +68,8 @@ public class RenamePanel extends JComponent { protected final RenameAction renameAction = new RenameAction(renameModel); private static final PreferencesEntry persistentPreserveExtension = Settings.forPackage(RenamePanel.class).entry("rename.extension.preserve").defaultValue("true"); - private static final PreferencesEntry persistentFormatExpression = Settings.forPackage(RenamePanel.class).entry("rename.format"); + private static final PreferencesEntry persistentEpisodeFormat = Settings.forPackage(RenamePanel.class).entry("rename.format.episode"); + private static final PreferencesEntry persistentMovieFormat = Settings.forPackage(RenamePanel.class).entry("rename.format.movie"); private static final PreferencesEntry persistentPreferredLanguage = Settings.forPackage(RenamePanel.class).entry("rename.language").defaultValue("en"); @@ -88,7 +91,14 @@ public class RenamePanel extends JComponent { try { // 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) { // illegal format, ignore } @@ -166,19 +176,21 @@ public class RenamePanel extends JComponent { @Override 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.setVisible(true); - switch (dialog.getSelectedOption()) { - case APPROVE: - renameModel.useFormatter(Episode.class, new EpisodeExpressionFormatter(dialog.getSelectedFormat().getExpression())); - persistentFormatExpression.setValue(dialog.getSelectedFormat().getExpression()); - break; - case USE_DEFAULT: - renameModel.useFormatter(Episode.class, null); - persistentFormatExpression.remove(); - break; + if (dialog.submit()) { + switch (dialog.getMode()) { + case Episode: + renameModel.useFormatter(Episode.class, new ExpressionFormatter(dialog.getFormat().getExpression(), EpisodeFormat.SeasonEpisode, Episode.class)); + persistentEpisodeFormat.setValue(dialog.getFormat().getExpression()); + break; + case Movie: + renameModel.useFormatter(MovieDescriptor.class, new ExpressionFormatter(dialog.getFormat().getExpression(), MovieFormat.NameYear, MovieDescriptor.class)); + persistentMovieFormat.setValue(dialog.getFormat().getExpression()); + break; + } } } }); diff --git a/source/net/sourceforge/filebot/web/CachedResource.java b/source/net/sourceforge/filebot/web/CachedResource.java new file mode 100644 index 00000000..e879e727 --- /dev/null +++ b/source/net/sourceforge/filebot/web/CachedResource.java @@ -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 { + + 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(); + } + +} diff --git a/source/net/sourceforge/filebot/web/EpisodeFormat.java b/source/net/sourceforge/filebot/web/EpisodeFormat.java index cd2146eb..e83d6bae 100644 --- a/source/net/sourceforge/filebot/web/EpisodeFormat.java +++ b/source/net/sourceforge/filebot/web/EpisodeFormat.java @@ -13,7 +13,6 @@ import java.util.regex.Pattern; public class EpisodeFormat extends Format { 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 includeSpecial; diff --git a/source/net/sourceforge/filebot/web/MovieDescriptor.java b/source/net/sourceforge/filebot/web/MovieDescriptor.java index 2382a842..33d4d26a 100644 --- a/source/net/sourceforge/filebot/web/MovieDescriptor.java +++ b/source/net/sourceforge/filebot/web/MovieDescriptor.java @@ -7,13 +7,12 @@ import java.util.Arrays; public class MovieDescriptor extends SearchResult { - private final int year; - private final int imdbId; + protected final int year; + protected final int imdbId; public MovieDescriptor(String name, int year, int imdbId) { super(name); - this.year = year; this.imdbId = imdbId; } @@ -48,9 +47,6 @@ public class MovieDescriptor extends SearchResult { @Override public String toString() { - if (year < 0) - return name; - return String.format("%s (%d)", name, year); } diff --git a/source/net/sourceforge/filebot/web/MovieFormat.java b/source/net/sourceforge/filebot/web/MovieFormat.java new file mode 100644 index 00000000..bc871667 --- /dev/null +++ b/source/net/sourceforge/filebot/web/MovieFormat.java @@ -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); + } + +} diff --git a/source/net/sourceforge/filebot/web/MoviePart.java b/source/net/sourceforge/filebot/web/MoviePart.java new file mode 100644 index 00000000..d9bc707a --- /dev/null +++ b/source/net/sourceforge/filebot/web/MoviePart.java @@ -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); + } + +} diff --git a/source/net/sourceforge/filebot/web/WebRequest.java b/source/net/sourceforge/filebot/web/WebRequest.java index 117986b1..0990bf12 100644 --- a/source/net/sourceforge/filebot/web/WebRequest.java +++ b/source/net/sourceforge/filebot/web/WebRequest.java @@ -106,13 +106,14 @@ public final class WebRequest { } - public static ByteBuffer fetch(URL resource) throws IOException { - return fetch(resource, null); + public static ByteBuffer fetchIfModified(URL resource, long ifModifiedSince) throws IOException { + return fetch(resource, ifModifiedSince, null); } - public static ByteBuffer fetch(URL url, Map requestParameters) throws IOException { + public static ByteBuffer fetch(URL url, long ifModifiedSince, Map requestParameters) throws IOException { URLConnection connection = url.openConnection(); + connection.setIfModifiedSince(ifModifiedSince); if (requestParameters != null) { for (Entry parameter : requestParameters.entrySet()) { @@ -123,7 +124,7 @@ public final class WebRequest { int contentLength = connection.getContentLength(); InputStream in = connection.getInputStream(); - ByteBufferOutputStream buffer = new ByteBufferOutputStream(contentLength >= 0 ? contentLength : 32 * 1024); + ByteBufferOutputStream buffer = new ByteBufferOutputStream(contentLength >= 0 ? contentLength : 4 * 1024); try { // read all @@ -138,6 +139,10 @@ public final class WebRequest { in.close(); } + // no data, e.g. If-Modified-Since requests + if (contentLength < 0 && buffer.getByteBuffer().remaining() == 0) + return null; + return buffer.getByteBuffer(); }