+ 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;
|
package net.sourceforge.filebot.subtitle;
|
||||||
|
|
||||||
|
|
||||||
|
import static java.util.regex.Pattern.*;
|
||||||
import static net.sourceforge.tuned.StringUtilities.*;
|
import static net.sourceforge.tuned.StringUtilities.*;
|
||||||
|
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
|
@ -12,7 +13,7 @@ import java.util.regex.Pattern;
|
||||||
public class SubViewerReader extends SubtitleReader {
|
public class SubViewerReader extends SubtitleReader {
|
||||||
|
|
||||||
private final DateFormat timeFormat = new SubtitleTimeFormat();
|
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) {
|
public SubViewerReader(Readable source) {
|
||||||
|
|
|
@ -44,7 +44,7 @@ public enum SubtitleFormat {
|
||||||
|
|
||||||
|
|
||||||
public ExtensionFileFilter getFilter() {
|
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 {
|
public abstract class SubtitleReader implements Iterator<SubtitleElement>, Closeable {
|
||||||
|
|
||||||
protected final Scanner scanner;
|
protected final Scanner scanner;
|
||||||
|
|
||||||
protected SubtitleElement current;
|
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.NotificationLogging.*;
|
||||||
import static net.sourceforge.filebot.ui.panel.subtitle.SubtitleUtilities.*;
|
import static net.sourceforge.filebot.ui.panel.subtitle.SubtitleUtilities.*;
|
||||||
import static net.sourceforge.tuned.FileUtilities.*;
|
import static net.sourceforge.tuned.FileUtilities.*;
|
||||||
|
import static net.sourceforge.tuned.ui.TunedUtilities.*;
|
||||||
|
|
||||||
import java.awt.event.ActionEvent;
|
import java.awt.event.ActionEvent;
|
||||||
import java.awt.event.MouseAdapter;
|
import java.awt.event.MouseAdapter;
|
||||||
|
@ -278,7 +279,7 @@ class SubtitleDownloadComponent extends JComponent {
|
||||||
viewer.getTitleLabel().setText("Subtitle Viewer");
|
viewer.getTitleLabel().setText("Subtitle Viewer");
|
||||||
viewer.getInfoLabel().setText(file.getPath());
|
viewer.getInfoLabel().setText(file.getPath());
|
||||||
|
|
||||||
viewer.setData(decode(file));
|
viewer.setData(decodeSubtitles(file));
|
||||||
viewer.setVisible(true);
|
viewer.setVisible(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,23 +290,61 @@ class SubtitleDownloadComponent extends JComponent {
|
||||||
// single file
|
// single file
|
||||||
MemoryFile file = (MemoryFile) selection[0];
|
MemoryFile file = (MemoryFile) selection[0];
|
||||||
|
|
||||||
JFileChooser fileChooser = new JFileChooser();
|
JFileChooser fc = new JFileChooser();
|
||||||
fileChooser.setSelectedFile(new File(validateFileName(file.getName())));
|
fc.setSelectedFile(new File(validateFileName(file.getName())));
|
||||||
|
|
||||||
if (fileChooser.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) {
|
if (fc.showSaveDialog(getWindow(this)) == JFileChooser.APPROVE_OPTION) {
|
||||||
write(file.getData(), fileChooser.getSelectedFile());
|
write(file.getData(), fc.getSelectedFile());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// multiple files
|
// multiple files
|
||||||
JFileChooser fileChooser = new JFileChooser();
|
JFileChooser fc = new JFileChooser();
|
||||||
fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
|
fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
|
||||||
|
|
||||||
if (fileChooser.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) {
|
if (fc.showSaveDialog(getWindow(this)) == JFileChooser.APPROVE_OPTION) {
|
||||||
File folder = fileChooser.getSelectedFile();
|
File folder = fc.getSelectedFile();
|
||||||
|
|
||||||
for (Object object : selection) {
|
for (Object object : selection) {
|
||||||
MemoryFile file = (MemoryFile) object;
|
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 ...
|
// Save As...
|
||||||
contextMenu.add(new AbstractAction("Save as ...") {
|
contextMenu.add(new AbstractAction("Save As...", ResourceManager.getIcon("action.save")) {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void actionPerformed(ActionEvent evt) {
|
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());
|
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;
|
package net.sourceforge.filebot.ui.panel.subtitle;
|
||||||
|
|
||||||
|
|
||||||
|
import static java.lang.Math.*;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.CharBuffer;
|
||||||
import java.nio.channels.FileChannel;
|
import java.nio.channels.FileChannel;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import com.ibm.icu.text.CharsetDetector;
|
import com.ibm.icu.text.CharsetDetector;
|
||||||
|
|
||||||
|
import net.sourceforge.filebot.subtitle.SubRipWriter;
|
||||||
import net.sourceforge.filebot.subtitle.SubtitleElement;
|
import net.sourceforge.filebot.subtitle.SubtitleElement;
|
||||||
import net.sourceforge.filebot.subtitle.SubtitleFormat;
|
import net.sourceforge.filebot.subtitle.SubtitleFormat;
|
||||||
import net.sourceforge.filebot.subtitle.SubtitleReader;
|
import net.sourceforge.filebot.subtitle.SubtitleReader;
|
||||||
|
@ -25,7 +30,7 @@ final class SubtitleUtilities {
|
||||||
/**
|
/**
|
||||||
* Detect charset and parse subtitle file even if extension is invalid
|
* 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
|
// detect charset and read text content
|
||||||
CharsetDetector detector = new CharsetDetector();
|
CharsetDetector detector = new CharsetDetector();
|
||||||
detector.setDeclaredEncoding("UTF-8");
|
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}.
|
* Write {@link ByteBuffer} to {@link File}.
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue