+ added support for re-encoding downloaded subtitles as .srt using a given charset and optionally changing the subtitle timing
This commit is contained in:
parent
332f371636
commit
38210a5565
Binary file not shown.
After Width: | Height: | Size: 833 B |
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -44,7 +44,7 @@ public enum SubtitleFormat {
|
|||
|
||||
|
||||
public ExtensionFileFilter getFilter() {
|
||||
return MediaTypes.getDefaultFilter("subtitle/" + this);
|
||||
return MediaTypes.getDefaultFilter("subtitle/" + this.name());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ import java.util.logging.Logger;
|
|||
public abstract class SubtitleReader implements Iterator<SubtitleElement>, Closeable {
|
||||
|
||||
protected final Scanner scanner;
|
||||
|
||||
protected SubtitleElement current;
|
||||
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Charset> encodings = new LinkedHashSet<Charset>(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<Charset> options) {
|
||||
encoding.setModel(new DefaultComboBoxModel(options.toArray()));
|
||||
}
|
||||
|
||||
|
||||
public Charset getSelectedEncoding() {
|
||||
return (Charset) encoding.getSelectedItem();
|
||||
}
|
||||
|
||||
|
||||
public void setFormatOptions(Set<SubtitleFormat> options) {
|
||||
format.setModel(new DefaultComboBoxModel(options.toArray()));
|
||||
}
|
||||
|
||||
|
||||
public SubtitleFormat getSelectedFormat() {
|
||||
return (SubtitleFormat) format.getSelectedItem();
|
||||
}
|
||||
|
||||
|
||||
public long getTimingOffset() {
|
||||
return (Integer) offset.getValue();
|
||||
}
|
||||
}
|
|
@ -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<SubtitleElement> decode(MemoryFile file) throws IOException {
|
||||
public static List<SubtitleElement> 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<SubtitleElement> 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}.
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue