From 38210a5565d9f46d1656801ffee28fd0f6fbf1a9 Mon Sep 17 00:00:00 2001 From: Reinhard Pointner Date: Tue, 6 Sep 2011 04:45:48 +0000 Subject: [PATCH] + added support for re-encoding downloaded subtitles as .srt using a given charset and optionally changing the subtitle timing --- .../filebot/resources/action.export.png | Bin 0 -> 833 bytes .../filebot/subtitle/SubRipWriter.java | 47 ++++++++++ .../filebot/subtitle/SubViewerReader.java | 3 +- .../filebot/subtitle/SubtitleFormat.java | 2 +- .../filebot/subtitle/SubtitleReader.java | 1 - .../subtitle/SubtitleDownloadComponent.java | 72 ++++++++++++--- .../panel/subtitle/SubtitleFileChooser.java | 85 ++++++++++++++++++ .../ui/panel/subtitle/SubtitleUtilities.java | 29 +++++- 8 files changed, 223 insertions(+), 16 deletions(-) create mode 100644 source/net/sourceforge/filebot/resources/action.export.png create mode 100644 source/net/sourceforge/filebot/subtitle/SubRipWriter.java create mode 100644 source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleFileChooser.java diff --git a/source/net/sourceforge/filebot/resources/action.export.png b/source/net/sourceforge/filebot/resources/action.export.png new file mode 100644 index 0000000000000000000000000000000000000000..68d546d919d4669be0e8dcbc06d8492983da6327 GIT binary patch literal 833 zcmV-H1HSx;P)wp=z_g8 zC<+6~ZbU($kY?&+I?Z&Mcb$69E@#`>*}sP_5S0XduiwXy@ACT)#u)x5sQHpPrH5lQ zNwH;|+jDJt&LuA`E~vHk4BTqCdA8|C4KW{JePQ0IyO&O0)}^U$8}c%)oUPtlzAQ6k z!#{V>V(snxG&BxH1-a!Rj6;pDTOkw*<(3`0P!$;=mzmEUy@W8vP_plIye275t5k$* zpTBLppP^M%t|`c=@%enTMwP70PEUnEKmgw7hvwFHsNB9DUN(OaPF%PhN9CE0L@bI@ z?lR=-&Lf0KsZ^5HJ2)bXj$#;>+e_+m(!n1Hew&vdK%1flOT#Pt;K`fKosZ2YazxfPP<5WP}_>r)H2}5C{MOYL;_xld~Kio0tXw z0FofW6mr>qZHj8AbIt|+fB*pKC!lX&2uh0bp`=jXn7#3kkxGt>8GH7+xs-Kz-byI4 zXO87=vTmMDh+mw=vK+QNdV$uiTm}mmC3M;Q!SQ7jw{_a;2!v>wrrx6;BI!0&X(co{ zs?XMzeIA*xMn^FRy8A!ZtTn9Yh>wj)w%I>|AwLH^UawepuOX_v;X3z=2i8V&U!+oz zk)}>OzP;4gq+6<9y{)A1pxf)0OwF)dYe%>G&AX0z&dvJ+f|&S64^}VFKB7(4q@|~- zpxxG&Y3sJfH@CDE(-akLZ8Q(A*jl}}y{kuQdwOI1kAQI}J2yJvgrzAOh5*1%5QLW`NjJkVtl4blsfvmU(PFXi!^6XZ z$K&AxP6sQBB8f33q*5uu7z08G5JDl6Bt@F01%_b+qtO^NnN08%OF>9i3eMTN00000 LNkvXXu0mjfh$4p* literal 0 HcmV?d00001 diff --git a/source/net/sourceforge/filebot/subtitle/SubRipWriter.java b/source/net/sourceforge/filebot/subtitle/SubRipWriter.java new file mode 100644 index 00000000..ce1514bd --- /dev/null +++ b/source/net/sourceforge/filebot/subtitle/SubRipWriter.java @@ -0,0 +1,47 @@ + +package net.sourceforge.filebot.subtitle; + + +import java.io.Closeable; +import java.io.IOException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Formatter; +import java.util.Locale; +import java.util.TimeZone; + + +public class SubRipWriter implements Closeable { + + private final DateFormat timeFormat; + private final Formatter out; + + private int lineNumber = 0; + + + public SubRipWriter(Appendable out) { + this.out = new Formatter(out, Locale.ROOT); + + // format used to create time stamps (e.g. 00:02:26,407 --> 00:02:31,356) + timeFormat = new SimpleDateFormat("HH:mm:ss,SSS", Locale.ROOT); + timeFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + } + + + public void write(SubtitleElement element) { + // write a single subtitle in SubRip format, e.g. + // 1 + // 00:00:20,000 --> 00:00:24,400 + // Altocumulus clouds occur between six thousand + out.format("%d%n", ++lineNumber); + out.format("%s --> %s%n", timeFormat.format(element.getStart()), timeFormat.format(element.getEnd())); + out.format("%s%n%n", element.getText()); + } + + + @Override + public void close() throws IOException { + out.close(); + } + +} diff --git a/source/net/sourceforge/filebot/subtitle/SubViewerReader.java b/source/net/sourceforge/filebot/subtitle/SubViewerReader.java index 37cdb06b..8a0b9b73 100644 --- a/source/net/sourceforge/filebot/subtitle/SubViewerReader.java +++ b/source/net/sourceforge/filebot/subtitle/SubViewerReader.java @@ -2,6 +2,7 @@ package net.sourceforge.filebot.subtitle; +import static java.util.regex.Pattern.*; import static net.sourceforge.tuned.StringUtilities.*; import java.text.DateFormat; @@ -12,7 +13,7 @@ import java.util.regex.Pattern; public class SubViewerReader extends SubtitleReader { private final DateFormat timeFormat = new SubtitleTimeFormat(); - private final Pattern newline = Pattern.compile(Pattern.quote("[br]"), Pattern.CASE_INSENSITIVE); + private final Pattern newline = compile(quote("[br]"), CASE_INSENSITIVE); public SubViewerReader(Readable source) { diff --git a/source/net/sourceforge/filebot/subtitle/SubtitleFormat.java b/source/net/sourceforge/filebot/subtitle/SubtitleFormat.java index 398101d6..a84c2ceb 100644 --- a/source/net/sourceforge/filebot/subtitle/SubtitleFormat.java +++ b/source/net/sourceforge/filebot/subtitle/SubtitleFormat.java @@ -44,7 +44,7 @@ public enum SubtitleFormat { public ExtensionFileFilter getFilter() { - return MediaTypes.getDefaultFilter("subtitle/" + this); + return MediaTypes.getDefaultFilter("subtitle/" + this.name()); } } diff --git a/source/net/sourceforge/filebot/subtitle/SubtitleReader.java b/source/net/sourceforge/filebot/subtitle/SubtitleReader.java index 23c7b3c6..7dab25fe 100644 --- a/source/net/sourceforge/filebot/subtitle/SubtitleReader.java +++ b/source/net/sourceforge/filebot/subtitle/SubtitleReader.java @@ -14,7 +14,6 @@ import java.util.logging.Logger; public abstract class SubtitleReader implements Iterator, Closeable { protected final Scanner scanner; - protected SubtitleElement current; diff --git a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleDownloadComponent.java b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleDownloadComponent.java index 41abf5a4..bdeb0f1e 100644 --- a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleDownloadComponent.java +++ b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleDownloadComponent.java @@ -6,6 +6,7 @@ import static net.sourceforge.filebot.MediaTypes.*; import static net.sourceforge.filebot.ui.NotificationLogging.*; import static net.sourceforge.filebot.ui.panel.subtitle.SubtitleUtilities.*; import static net.sourceforge.tuned.FileUtilities.*; +import static net.sourceforge.tuned.ui.TunedUtilities.*; import java.awt.event.ActionEvent; import java.awt.event.MouseAdapter; @@ -278,7 +279,7 @@ class SubtitleDownloadComponent extends JComponent { viewer.getTitleLabel().setText("Subtitle Viewer"); viewer.getInfoLabel().setText(file.getPath()); - viewer.setData(decode(file)); + viewer.setData(decodeSubtitles(file)); viewer.setVisible(true); } @@ -289,23 +290,61 @@ class SubtitleDownloadComponent extends JComponent { // single file MemoryFile file = (MemoryFile) selection[0]; - JFileChooser fileChooser = new JFileChooser(); - fileChooser.setSelectedFile(new File(validateFileName(file.getName()))); + JFileChooser fc = new JFileChooser(); + fc.setSelectedFile(new File(validateFileName(file.getName()))); - if (fileChooser.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) { - write(file.getData(), fileChooser.getSelectedFile()); + if (fc.showSaveDialog(getWindow(this)) == JFileChooser.APPROVE_OPTION) { + write(file.getData(), fc.getSelectedFile()); } } else { // multiple files - JFileChooser fileChooser = new JFileChooser(); - fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + JFileChooser fc = new JFileChooser(); + fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); - if (fileChooser.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) { - File folder = fileChooser.getSelectedFile(); + if (fc.showSaveDialog(getWindow(this)) == JFileChooser.APPROVE_OPTION) { + File folder = fc.getSelectedFile(); for (Object object : selection) { MemoryFile file = (MemoryFile) object; - write(file.getData(), new File(folder, validateFileName(file.getName()))); + File destination = new File(folder, validateFileName(file.getName())); + write(file.getData(), destination); + } + } + } + } catch (IOException e) { + UILogger.log(Level.WARNING, e.getMessage(), e); + } + } + + + private void export(Object[] selection) { + try { + if (selection.length == 1) { + // single file + MemoryFile file = (MemoryFile) selection[0]; + + SubtitleFileChooser sf = new SubtitleFileChooser(); + + // normalize name and auto-adjust extension + String ext = sf.getSelectedFormat().getFilter().extensions()[0]; + String name = validateFileName(getNameWithoutExtension(file.getName())); + sf.setSelectedFile(new File(name + "." + ext)); + + if (sf.showSaveDialog(getWindow(this)) == JFileChooser.APPROVE_OPTION) { + exportSubtitles(decodeSubtitles(file), sf.getSelectedFile(), sf.getSelectedEncoding(), sf.getSelectedFormat(), sf.getTimingOffset()); + } + } else { + // multiple files + SubtitleFileChooser sf = new SubtitleFileChooser(); + sf.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + + if (sf.showSaveDialog(getWindow(this)) == JFileChooser.APPROVE_OPTION) { + File folder = sf.getSelectedFile(); + + for (Object object : selection) { + MemoryFile file = (MemoryFile) object; + File destination = new File(folder, validateFileName(file.getName())); + exportSubtitles(decodeSubtitles(file), destination, sf.getSelectedEncoding(), sf.getSelectedFormat(), sf.getTimingOffset()); } } } @@ -446,8 +485,8 @@ class SubtitleDownloadComponent extends JComponent { } }); - // Save as ... - contextMenu.add(new AbstractAction("Save as ...") { + // Save As... + contextMenu.add(new AbstractAction("Save As...", ResourceManager.getIcon("action.save")) { @Override public void actionPerformed(ActionEvent evt) { @@ -455,6 +494,15 @@ class SubtitleDownloadComponent extends JComponent { } }); + // Export... + contextMenu.add(new AbstractAction("Export...", ResourceManager.getIcon("action.export")) { + + @Override + public void actionPerformed(ActionEvent evt) { + export(selection); + } + }); + contextMenu.show(e.getComponent(), e.getX(), e.getY()); } } diff --git a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleFileChooser.java b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleFileChooser.java new file mode 100644 index 00000000..814f3b99 --- /dev/null +++ b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleFileChooser.java @@ -0,0 +1,85 @@ + +package net.sourceforge.filebot.ui.panel.subtitle; + + +import static java.util.Collections.*; + +import java.nio.charset.Charset; +import java.util.LinkedHashSet; +import java.util.Set; + +import javax.swing.DefaultComboBoxModel; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.SpinnerNumberModel; + +import net.miginfocom.swing.MigLayout; +import net.sourceforge.filebot.subtitle.SubtitleFormat; + + +public class SubtitleFileChooser extends JFileChooser { + + protected final JComboBox format = new JComboBox(); + protected final JComboBox encoding = new JComboBox(); + protected final JSpinner offset = new JSpinner(new SpinnerNumberModel(0, -14400000, 14400000, 100)); + + + public SubtitleFileChooser() { + setAccessory(createAcessory()); + setDefaultOptions(); + } + + + protected void setDefaultOptions() { + setFormatOptions(singleton(SubtitleFormat.SubRip)); + + Set encodings = new LinkedHashSet(2); + encodings.add(Charset.forName("UTF-8")); // UTF-8 as default charset + encodings.add(Charset.defaultCharset()); // allow default system encoding to be used as well + setEncodingOptions(encodings); + } + + + protected JComponent createAcessory() { + JPanel acessory = new JPanel(new MigLayout("nogrid")); + + acessory.add(new JLabel("Encoding:"), "wrap rel"); + acessory.add(encoding, "sg w, wrap para"); + acessory.add(new JLabel("Format:"), "wrap rel"); + acessory.add(format, "sg w, wrap para"); + acessory.add(new JLabel("Timing Offset:"), "wrap rel"); + acessory.add(offset, "wmax 50px"); + acessory.add(new JLabel("ms")); + + return acessory; + } + + + public void setEncodingOptions(Set options) { + encoding.setModel(new DefaultComboBoxModel(options.toArray())); + } + + + public Charset getSelectedEncoding() { + return (Charset) encoding.getSelectedItem(); + } + + + public void setFormatOptions(Set options) { + format.setModel(new DefaultComboBoxModel(options.toArray())); + } + + + public SubtitleFormat getSelectedFormat() { + return (SubtitleFormat) format.getSelectedItem(); + } + + + public long getTimingOffset() { + return (Integer) offset.getValue(); + } +} diff --git a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleUtilities.java b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleUtilities.java index 7c7f05d7..3a6b6f0d 100644 --- a/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleUtilities.java +++ b/source/net/sourceforge/filebot/ui/panel/subtitle/SubtitleUtilities.java @@ -2,18 +2,23 @@ package net.sourceforge.filebot.ui.panel.subtitle; +import static java.lang.Math.*; + import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.StringReader; import java.nio.ByteBuffer; +import java.nio.CharBuffer; import java.nio.channels.FileChannel; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import com.ibm.icu.text.CharsetDetector; +import net.sourceforge.filebot.subtitle.SubRipWriter; import net.sourceforge.filebot.subtitle.SubtitleElement; import net.sourceforge.filebot.subtitle.SubtitleFormat; import net.sourceforge.filebot.subtitle.SubtitleReader; @@ -25,7 +30,7 @@ final class SubtitleUtilities { /** * Detect charset and parse subtitle file even if extension is invalid */ - public static List decode(MemoryFile file) throws IOException { + public static List decodeSubtitles(MemoryFile file) throws IOException { // detect charset and read text content CharsetDetector detector = new CharsetDetector(); detector.setDeclaredEncoding("UTF-8"); @@ -68,6 +73,28 @@ final class SubtitleUtilities { } + /** + * Write a subtitle file to disk + */ + public static void exportSubtitles(List data, File destination, Charset encoding, SubtitleFormat format, long timingOffset) throws IOException { + if (format != SubtitleFormat.SubRip) + throw new IllegalArgumentException("Format not supported"); + + StringBuilder buffer = new StringBuilder(4 * 1024); + SubRipWriter out = new SubRipWriter(buffer); + + for (SubtitleElement it : data) { + if (timingOffset != 0) + it = new SubtitleElement(max(0, it.getStart() + timingOffset), max(0, it.getEnd() + timingOffset), it.getText()); + + out.write(it); + } + + // write to file + write(encoding.encode(CharBuffer.wrap(buffer)), destination); + } + + /** * Write {@link ByteBuffer} to {@link File}. */