mirror of
https://github.com/mitb-archive/filebot
synced 2024-12-23 08:18:52 -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:
parent
ff8eed5af2
commit
339cbfd49e
BIN
fw/dialog.switch.png
Normal file
BIN
fw/dialog.switch.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 54 KiB |
@ -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<File, Episode> 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<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()));
|
||||
|
||||
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]);
|
||||
}
|
||||
|
@ -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> 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("(?<!\\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")
|
||||
public Object getGeneralMediaInfo() {
|
||||
return new AssociativeScriptObject(getMediaInfo().snapshot(StreamKind.General, 0));
|
||||
@ -209,7 +294,13 @@ public class EpisodeBindingBean {
|
||||
|
||||
@Define("episode")
|
||||
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() {
|
||||
// make sure media file is defined
|
||||
checkMediaFile();
|
||||
@ -289,4 +403,15 @@ public class EpisodeBindingBean {
|
||||
cache.put(new Element(file, 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));
|
||||
}
|
||||
};
|
||||
|
||||
}
|
@ -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 |
BIN
source/net/sourceforge/filebot/resources/dialog.switch.png
Normal file
BIN
source/net/sourceforge/filebot/resources/dialog.switch.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.0 KiB |
@ -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<String> getSampleExpressions() {
|
||||
private List<String> getSampleExpressions() {
|
||||
ResourceBundle bundle = ResourceBundle.getBundle(getClass().getName());
|
||||
TreeMap<String, String> expressions = new TreeMap<String, String>();
|
||||
|
||||
// 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<Object, Object> 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);
|
||||
}
|
@ -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
|
@ -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
|
@ -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)}
|
@ -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) {
|
@ -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<String> 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<String> persistentSampleEpisode = Settings.forPackage(EpisodeFormatDialog.class).entry("format.sample.episode");
|
||||
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();
|
||||
private static final PreferencesEntry<String> 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<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) {
|
||||
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<String, Void>() {
|
||||
|
||||
@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<String> recent = new LinkedHashSet<String>();
|
||||
|
||||
// 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));
|
||||
}
|
@ -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}
|
@ -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);
|
||||
|
@ -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, 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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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<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");
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
56
source/net/sourceforge/filebot/web/CachedResource.java
Normal file
56
source/net/sourceforge/filebot/web/CachedResource.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
97
source/net/sourceforge/filebot/web/MovieFormat.java
Normal file
97
source/net/sourceforge/filebot/web/MovieFormat.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
33
source/net/sourceforge/filebot/web/MoviePart.java
Normal file
33
source/net/sourceforge/filebot/web/MoviePart.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
@ -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<String, String> requestParameters) throws IOException {
|
||||
public static ByteBuffer fetch(URL url, long ifModifiedSince, Map<String, String> requestParameters) throws IOException {
|
||||
URLConnection connection = url.openConnection();
|
||||
connection.setIfModifiedSince(ifModifiedSince);
|
||||
|
||||
if (requestParameters != null) {
|
||||
for (Entry<String, String> 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();
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user