diff --git a/CHANGELOG.md b/CHANGELOG.md
index 36cc4762d8..37f5d41e7d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -35,6 +35,7 @@
- **FEATURE:** Mit der Lucene-Suche können mittels des `duplicate`-Boolean Parameters Filmduplikate berücksichtigt werden.
- **FEATURE:** Via `Ansicht/Übersicht aller Duplikate anzeigen...` werden in einem Dialog per Sender alle vorhandenen Duplikate dargestellt zzgl. der zugeordneten Filme.
- **FEATURE:** Geschwindigkeitsoptimierungen für die moderne Suche.
+- **FEATURE:** Mittels Menü `Downloads/Unterttiteldatei zu Video hinzufügen...` kann eine vorhandene Untertiteldatei mit der korrekten Sprachzuordnung zu einem Video hinzugefügt werden. Moderne Videoplayer erkennen die Untertitelspur automatisch und man muss keine separaten Untertiteldateien mehr verwalten.
# **14.1.0**
- JDK 21 wird nun mitgeliefert. Behebt primär Darstellungsfehler von Java Apps unter Windows.
- **macOS/Windows:** ffmpeg 7.0 ist nun enthalten.
diff --git a/pom.xml b/pom.xml
index 9195d75839..dc93386993 100755
--- a/pom.xml
+++ b/pom.xml
@@ -100,6 +100,7 @@
11.2.1
3.5.2
33.3.0-jre
+ 76.1
1.1.2
2.17.2
2023.09.10
@@ -147,6 +148,12 @@
+
+ com.ibm.icu
+ icu4j
+ ${icu4j.version}
+
+
com.github.lgooddatepicker
LGoodDatePicker
diff --git a/src/main/java/mediathek/gui/actions/MergeSubtitleWithVideoAction.java b/src/main/java/mediathek/gui/actions/MergeSubtitleWithVideoAction.java
new file mode 100644
index 0000000000..04b1598867
--- /dev/null
+++ b/src/main/java/mediathek/gui/actions/MergeSubtitleWithVideoAction.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2024 derreisende77.
+ * This code was developed as part of the MediathekView project https://github.com/mediathekview/MediathekView
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package mediathek.gui.actions;
+
+import mediathek.gui.dialog.subripmerge.MergeSubripVideoDialog;
+import mediathek.mainwindow.MediathekGui;
+
+import javax.swing.*;
+import java.awt.event.ActionEvent;
+
+public class MergeSubtitleWithVideoAction extends AbstractAction {
+ private final MediathekGui ui;
+
+ public MergeSubtitleWithVideoAction(MediathekGui mediathekGui) {
+ this.ui = mediathekGui;
+ putValue(NAME, "Untertiteldatei zu Video hinzufügen...");
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ MergeSubripVideoDialog dlg = new MergeSubripVideoDialog(ui);
+ dlg.setVisible(true);
+ }
+}
diff --git a/src/main/java/mediathek/gui/dialog/subripmerge/MergeSubripVideoDialog.java b/src/main/java/mediathek/gui/dialog/subripmerge/MergeSubripVideoDialog.java
new file mode 100644
index 0000000000..7440e0cddf
--- /dev/null
+++ b/src/main/java/mediathek/gui/dialog/subripmerge/MergeSubripVideoDialog.java
@@ -0,0 +1,364 @@
+/*
+ * Copyright (c) 2024 derreisende77.
+ * This code was developed as part of the MediathekView project https://github.com/mediathekview/MediathekView
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package mediathek.gui.dialog.subripmerge;
+
+import com.github.kokorin.jaffree.ffmpeg.FFmpeg;
+import com.github.kokorin.jaffree.ffmpeg.UrlInput;
+import com.github.kokorin.jaffree.ffmpeg.UrlOutput;
+import mediathek.config.Konstanten;
+import mediathek.tool.FileDialogs;
+import mediathek.tool.GuiFunktionenProgramme;
+import mediathek.tool.LanguageCode;
+import mediathek.tool.SwingErrorDialog;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.jdesktop.swingx.JXBusyLabel;
+
+import javax.swing.*;
+import javax.swing.border.EmptyBorder;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import java.awt.*;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @author christianfranzke
+ */
+public class MergeSubripVideoDialog extends JDialog {
+ private static final Logger logger = LogManager.getLogger();
+ private static final Pattern PATTERN = Pattern.compile("\\[(.*?)]");
+
+ public MergeSubripVideoDialog(Window owner) {
+ super(owner);
+ initComponents();
+
+ getRootPane().setDefaultButton(btnMerge);
+ busyLabel.setVisible(false);
+ btnMerge.setEnabled(false);
+
+ fillLanguageComboBox();
+
+ btnCancel.addActionListener(_ -> dispose());
+
+ setupTextFieldListener();
+
+ btnSelectInputSubrip.addActionListener(_ -> {
+ var file = FileDialogs.chooseLoadFileLocation(this, "Untertitel wählen", "");
+ if (file != null) {
+ var fileStr = file.getAbsolutePath();
+ if (!fileStr.toLowerCase().endsWith(".srt")) {
+ JOptionPane.showMessageDialog(this, "Untertiteldatei muss auf .srt enden.", Konstanten.PROGRAMMNAME, JOptionPane.ERROR_MESSAGE);
+ tfSubripFilePath.setText("");
+ }
+ else {
+ tfSubripFilePath.setText(file.getAbsolutePath());
+ }
+ }
+ });
+ btnSelectInputVideo.addActionListener(_ -> {
+ var file = FileDialogs.chooseLoadFileLocation(this, "Video wählen", "");
+ if (file != null) {
+ tfVideoFilePath.setText(file.getAbsolutePath());
+ }
+ });
+
+ btnSelectVideoOutputPath.addActionListener(_ -> {
+ var file = FileDialogs.chooseSaveFileLocation(this, "Videospeicherort wählen", "");
+ if (file != null) {
+ tfVideoOutputPath.setText(file.getAbsolutePath());
+ }
+ });
+
+ btnMerge.addActionListener(_ -> {
+ try {
+ var lang = (String) cbLanguage.getSelectedItem();
+ if (lang == null)
+ throw new IllegalArgumentException("Native language selected is null");
+
+ Matcher matcher = PATTERN.matcher(lang);
+
+ if (matcher.find()) {
+ lang = matcher.group(1);
+ } else {
+ throw new IllegalArgumentException("Could not get ISO 639 3 letter code");
+ }
+
+ busyLabel.setVisible(true);
+ busyLabel.setBusy(true);
+ btnMerge.setEnabled(false);
+ btnCancel.setEnabled(false);
+
+ var ffmpegPath = GuiFunktionenProgramme.findExecutableOnPath("ffmpeg").getParent();
+ var ffmpeg = FFmpeg.atPath(ffmpegPath)
+ .setOverwriteOutput(true)
+ .addArgument("-xerror")
+ .addInput(UrlInput.fromUrl(tfVideoFilePath.getText()))
+ .addInput(UrlInput.fromUrl(tfSubripFilePath.getText()))
+ .addOutput(UrlOutput.toUrl(tfVideoOutputPath.getText()))
+ .addArguments("-c", "copy")
+ .addArguments("-c:s", "mov_text")
+ .addArgument("-metadata:s:s:0")
+ .addArgument("language=" + lang);
+ ffmpeg.executeAsync().toCompletableFuture()
+ .thenAccept(_ -> SwingUtilities.invokeLater(() -> {
+ shutdownMergeProcess();
+ JOptionPane.showMessageDialog(MergeSubripVideoDialog.this, "Das Zusammenführen war erfolgreich", Konstanten.PROGRAMMNAME, JOptionPane.INFORMATION_MESSAGE);
+ dispose();
+ }))
+ .exceptionally(ex -> {
+ SwingUtilities.invokeLater(() -> {
+ shutdownMergeProcess();
+ SwingErrorDialog.showExceptionMessage(MergeSubripVideoDialog.this, "Der Vorgang war fehlerhaft", ex);
+ dispose();
+ });
+ return null;
+ });
+ } catch (Exception e) {
+ logger.error("Error occured while merging video", e);
+ shutdownMergeProcess();
+ SwingErrorDialog.showExceptionMessage(this, "Es ist ein Fehler aufgetreten.", e);
+ }
+ });
+ }
+
+ private void setupTextFieldListener() {
+ DocumentListener documentListener = new DocumentListener() {
+ @Override
+ public void insertUpdate(DocumentEvent e) {
+ updateButtonState();
+ }
+
+ @Override
+ public void removeUpdate(DocumentEvent e) {
+ updateButtonState();
+ }
+
+ @Override
+ public void changedUpdate(DocumentEvent e) {
+ updateButtonState();
+ }
+
+ private void updateButtonState() {
+ boolean allFieldsFilled = !tfSubripFilePath.getText().trim().isEmpty() &&
+ !tfVideoFilePath.getText().trim().isEmpty() &&
+ !tfVideoOutputPath.getText().trim().isEmpty();
+ btnMerge.setEnabled(allFieldsFilled);
+ }
+ };
+
+ tfSubripFilePath.getDocument().addDocumentListener(documentListener);
+ tfVideoFilePath.getDocument().addDocumentListener(documentListener);
+ tfVideoOutputPath.getDocument().addDocumentListener(documentListener);
+ }
+
+ private void shutdownMergeProcess() {
+ busyLabel.setBusy(false);
+ busyLabel.setVisible(false);
+ btnCancel.setEnabled(true);
+ }
+
+ public void fillLanguageComboBox() {
+ List languages = new ArrayList<>();
+ for (var item : LanguageCode.values()) {
+ var entry = String.format("%s [%s]", item.nativeName(), item.getISO3Language());
+ languages.add(entry);
+ }
+ cbLanguage.setModel(new DefaultComboBoxModel<>(languages.toArray(new String[0])));
+ }
+
+ private void initComponents() {
+ // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents @formatter:off
+ // Generated using JFormDesigner non-commercial license
+ var dialogPane = new JPanel();
+ var contentPanel = new JPanel();
+ var label1 = new JLabel();
+ tfSubripFilePath = new JTextField();
+ btnSelectInputSubrip = new JButton();
+ var label2 = new JLabel();
+ tfVideoFilePath = new JTextField();
+ btnSelectInputVideo = new JButton();
+ var label3 = new JLabel();
+ cbLanguage = new JComboBox<>();
+ var label4 = new JLabel();
+ tfVideoOutputPath = new JTextField();
+ btnSelectVideoOutputPath = new JButton();
+ busyLabel = new JXBusyLabel();
+ var buttonBar = new JPanel();
+ btnCancel = new JButton();
+ btnMerge = new JButton();
+
+ //======== this ========
+ setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
+ setModal(true);
+ setTitle("Untertitel zu Video hinzuf\u00fcgen"); //NON-NLS
+ var contentPane = getContentPane();
+ contentPane.setLayout(new BorderLayout());
+
+ //======== dialogPane ========
+ {
+ dialogPane.setBorder(new EmptyBorder(12, 12, 12, 12));
+ dialogPane.setLayout(new BorderLayout());
+
+ //======== contentPanel ========
+ {
+
+ //---- label1 ----
+ label1.setText("Untertitel-Datei:"); //NON-NLS
+ label1.setHorizontalAlignment(SwingConstants.RIGHT);
+
+ //---- btnSelectInputSubrip ----
+ btnSelectInputSubrip.setText("..."); //NON-NLS
+
+ //---- label2 ----
+ label2.setText("Video-Datei:"); //NON-NLS
+ label2.setHorizontalAlignment(SwingConstants.RIGHT);
+
+ //---- btnSelectInputVideo ----
+ btnSelectInputVideo.setText("..."); //NON-NLS
+
+ //---- label3 ----
+ label3.setText("Sprache:"); //NON-NLS
+ label3.setHorizontalAlignment(SwingConstants.RIGHT);
+
+ //---- cbLanguage ----
+ cbLanguage.setToolTipText("Sprache der Untertitel"); //NON-NLS
+
+ //---- label4 ----
+ label4.setText("Zieldatei:"); //NON-NLS
+ label4.setHorizontalAlignment(SwingConstants.RIGHT);
+
+ //---- btnSelectVideoOutputPath ----
+ btnSelectVideoOutputPath.setText("..."); //NON-NLS
+
+ //---- busyLabel ----
+ busyLabel.setText("F\u00fchre Video und Untertitel zusammen"); //NON-NLS
+
+ GroupLayout contentPanelLayout = new GroupLayout(contentPanel);
+ contentPanel.setLayout(contentPanelLayout);
+ contentPanelLayout.setHorizontalGroup(
+ contentPanelLayout.createParallelGroup()
+ .addGroup(contentPanelLayout.createSequentialGroup()
+ .addContainerGap()
+ .addGroup(contentPanelLayout.createParallelGroup()
+ .addGroup(contentPanelLayout.createSequentialGroup()
+ .addGroup(contentPanelLayout.createParallelGroup(GroupLayout.Alignment.TRAILING, false)
+ .addComponent(label2, GroupLayout.DEFAULT_SIZE, 104, Short.MAX_VALUE)
+ .addComponent(label3, GroupLayout.PREFERRED_SIZE, 98, GroupLayout.PREFERRED_SIZE)
+ .addComponent(label1, GroupLayout.DEFAULT_SIZE, 104, Short.MAX_VALUE))
+ .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
+ .addGroup(contentPanelLayout.createParallelGroup()
+ .addGroup(contentPanelLayout.createSequentialGroup()
+ .addComponent(tfVideoFilePath)
+ .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
+ .addComponent(btnSelectInputVideo, GroupLayout.PREFERRED_SIZE, 33, GroupLayout.PREFERRED_SIZE))
+ .addGroup(contentPanelLayout.createSequentialGroup()
+ .addComponent(tfSubripFilePath)
+ .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
+ .addComponent(btnSelectInputSubrip, GroupLayout.PREFERRED_SIZE, 33, GroupLayout.PREFERRED_SIZE))
+ .addGroup(contentPanelLayout.createSequentialGroup()
+ .addComponent(cbLanguage, GroupLayout.PREFERRED_SIZE, 155, GroupLayout.PREFERRED_SIZE)
+ .addGap(0, 0, Short.MAX_VALUE))))
+ .addGroup(GroupLayout.Alignment.TRAILING, contentPanelLayout.createSequentialGroup()
+ .addGroup(contentPanelLayout.createParallelGroup(GroupLayout.Alignment.TRAILING)
+ .addGroup(contentPanelLayout.createSequentialGroup()
+ .addComponent(label4, GroupLayout.PREFERRED_SIZE, 104, GroupLayout.PREFERRED_SIZE)
+ .addGap(6, 6, 6)
+ .addComponent(tfVideoOutputPath, GroupLayout.PREFERRED_SIZE, 368, GroupLayout.PREFERRED_SIZE))
+ .addGroup(GroupLayout.Alignment.LEADING, contentPanelLayout.createSequentialGroup()
+ .addGap(20, 20, 20)
+ .addComponent(busyLabel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)))
+ .addGap(6, 6, 6)
+ .addComponent(btnSelectVideoOutputPath, GroupLayout.PREFERRED_SIZE, 33, GroupLayout.PREFERRED_SIZE)))
+ .addContainerGap())
+ );
+ contentPanelLayout.setVerticalGroup(
+ contentPanelLayout.createParallelGroup()
+ .addGroup(contentPanelLayout.createSequentialGroup()
+ .addContainerGap()
+ .addGroup(contentPanelLayout.createParallelGroup(GroupLayout.Alignment.BASELINE)
+ .addComponent(btnSelectInputSubrip)
+ .addComponent(tfSubripFilePath, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
+ .addComponent(label1))
+ .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
+ .addGroup(contentPanelLayout.createParallelGroup(GroupLayout.Alignment.BASELINE)
+ .addComponent(label3)
+ .addComponent(cbLanguage, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE))
+ .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
+ .addGroup(contentPanelLayout.createParallelGroup(GroupLayout.Alignment.BASELINE)
+ .addComponent(btnSelectInputVideo)
+ .addComponent(label2)
+ .addComponent(tfVideoFilePath, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE))
+ .addGap(18, 18, 18)
+ .addGroup(contentPanelLayout.createParallelGroup()
+ .addGroup(contentPanelLayout.createSequentialGroup()
+ .addGap(7, 7, 7)
+ .addComponent(label4))
+ .addComponent(tfVideoOutputPath, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
+ .addComponent(btnSelectVideoOutputPath))
+ .addGap(18, 18, 18)
+ .addComponent(busyLabel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
+ .addContainerGap(GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
+ );
+ }
+ dialogPane.add(contentPanel, BorderLayout.CENTER);
+
+ //======== buttonBar ========
+ {
+ buttonBar.setBorder(new EmptyBorder(12, 0, 0, 0));
+ buttonBar.setLayout(new GridBagLayout());
+ ((GridBagLayout)buttonBar.getLayout()).columnWidths = new int[] {0, 0, 80};
+ ((GridBagLayout)buttonBar.getLayout()).columnWeights = new double[] {1.0, 0.0, 0.0};
+
+ //---- btnCancel ----
+ btnCancel.setText("Abbrechen"); //NON-NLS
+ buttonBar.add(btnCancel, new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0,
+ GridBagConstraints.CENTER, GridBagConstraints.BOTH,
+ new Insets(0, 0, 0, 5), 0, 0));
+
+ //---- btnMerge ----
+ btnMerge.setText("Zusammenf\u00fchren"); //NON-NLS
+ buttonBar.add(btnMerge, new GridBagConstraints(2, 0, 1, 1, 0.0, 0.0,
+ GridBagConstraints.CENTER, GridBagConstraints.BOTH,
+ new Insets(0, 0, 0, 0), 0, 0));
+ }
+ dialogPane.add(buttonBar, BorderLayout.SOUTH);
+ }
+ contentPane.add(dialogPane, BorderLayout.CENTER);
+ pack();
+ setLocationRelativeTo(getOwner());
+ // JFormDesigner - End of component initialization //GEN-END:initComponents @formatter:on
+ }
+
+ // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables @formatter:off
+ // Generated using JFormDesigner non-commercial license
+ private JTextField tfSubripFilePath;
+ private JButton btnSelectInputSubrip;
+ private JTextField tfVideoFilePath;
+ private JButton btnSelectInputVideo;
+ private JComboBox cbLanguage;
+ private JTextField tfVideoOutputPath;
+ private JButton btnSelectVideoOutputPath;
+ private JXBusyLabel busyLabel;
+ private JButton btnCancel;
+ private JButton btnMerge;
+ // JFormDesigner - End of variables declaration //GEN-END:variables @formatter:on
+}
diff --git a/src/main/java/mediathek/gui/dialog/subripmerge/MergeSubripVideoDialog.jfd b/src/main/java/mediathek/gui/dialog/subripmerge/MergeSubripVideoDialog.jfd
new file mode 100644
index 0000000000..4b6e0cb730
--- /dev/null
+++ b/src/main/java/mediathek/gui/dialog/subripmerge/MergeSubripVideoDialog.jfd
@@ -0,0 +1,130 @@
+JFDML JFormDesigner: "8.2.4.0.393" Java: "21.0.4" encoding: "UTF-8"
+
+new FormModel {
+ contentType: "form/swing"
+ root: new FormRoot {
+ add( new FormWindow( "javax.swing.JDialog", new FormLayoutManager( class java.awt.BorderLayout ) ) {
+ name: "this"
+ "defaultCloseOperation": 2
+ "modal": true
+ "title": "Untertitel zu Video hinzufügen"
+ add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class java.awt.BorderLayout ) ) {
+ name: "dialogPane"
+ "border": new javax.swing.border.EmptyBorder( 12, 12, 12, 12 )
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": true
+ }
+ add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class org.jdesktop.layout.GroupLayout ) {
+ "$horizontalGroup": "par l {seq l {space :::p, par l {seq l {par t:::p {comp label2::::104:x, comp label3::t:p:98:p, comp label1::::104:x}, space :::p, par l {seq l {comp tfVideoFilePath:::::x, space :::p, comp btnSelectInputVideo:::p:33:p}, seq {comp tfSubripFilePath:::::x, space :::p, comp btnSelectInputSubrip:::p:33:p}, seq {comp cbLanguage:::p:155:p, space :0:0:x}}}, seq t {par t {seq {comp label4:::p:104:p, space :6:6:p, comp tfVideoOutputPath:::p:368:p}, seq l {space :p:20:p, comp busyLabel:::p::p}}, space :6:6:p, comp btnSelectVideoOutputPath:::p:33:p}}, space :::p}}"
+ "$verticalGroup": "par l {seq l {space :::p, par b {comp btnSelectInputSubrip::b:p::p, comp tfSubripFilePath::b:p::p, comp label1::b:p::p}, space :::p, par b {comp label3::b:p::p, comp cbLanguage::b:p::p}, space :::p, par b {comp btnSelectInputVideo::b:p::p, comp label2::b:p::p, comp tfVideoFilePath::b:p::p}, space s:::p, par l {seq l {space :7:7:p, comp label4:::p::p}, comp tfVideoOutputPath:::p::p, comp btnSelectVideoOutputPath:::p::p}, space s:::p, comp busyLabel:::p::p, space :::x}}"
+ } ) {
+ name: "contentPanel"
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": true
+ }
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "label1"
+ "text": "Untertitel-Datei:"
+ "horizontalAlignment": 4
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": true
+ }
+ } )
+ add( new FormComponent( "javax.swing.JTextField" ) {
+ name: "tfSubripFilePath"
+ } )
+ add( new FormComponent( "javax.swing.JButton" ) {
+ name: "btnSelectInputSubrip"
+ "text": "..."
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "label2"
+ "text": "Video-Datei:"
+ "horizontalAlignment": 4
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": true
+ }
+ } )
+ add( new FormComponent( "javax.swing.JTextField" ) {
+ name: "tfVideoFilePath"
+ } )
+ add( new FormComponent( "javax.swing.JButton" ) {
+ name: "btnSelectInputVideo"
+ "text": "..."
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "label3"
+ "text": "Sprache:"
+ "horizontalAlignment": 4
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": true
+ }
+ } )
+ add( new FormComponent( "javax.swing.JComboBox" ) {
+ name: "cbLanguage"
+ "toolTipText": "Sprache der Untertitel"
+ auxiliary() {
+ "JavaCodeGenerator.typeParameters": "String"
+ }
+ } )
+ add( new FormComponent( "javax.swing.JLabel" ) {
+ name: "label4"
+ "text": "Zieldatei:"
+ "horizontalAlignment": 4
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": true
+ }
+ } )
+ add( new FormComponent( "javax.swing.JTextField" ) {
+ name: "tfVideoOutputPath"
+ } )
+ add( new FormComponent( "javax.swing.JButton" ) {
+ name: "btnSelectVideoOutputPath"
+ "text": "..."
+ } )
+ add( new FormComponent( "org.jdesktop.swingx.JXBusyLabel" ) {
+ name: "busyLabel"
+ "text": "Führe Video und Untertitel zusammen"
+ } )
+ }, new FormLayoutConstraints( class java.lang.String ) {
+ "value": "Center"
+ } )
+ add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class java.awt.GridBagLayout ) {
+ "$columnSpecs": "0:1.0, 0, 80"
+ "$rowSpecs": "0"
+ "$hGap": 5
+ "$vGap": 5
+ } ) {
+ name: "buttonBar"
+ "border": new javax.swing.border.EmptyBorder( 12, 0, 0, 0 )
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": true
+ }
+ add( new FormComponent( "javax.swing.JButton" ) {
+ name: "btnCancel"
+ "text": "Abbrechen"
+ }, new FormLayoutConstraints( class com.jformdesigner.runtime.GridBagConstraintsEx ) {
+ "gridx": 1
+ } )
+ add( new FormComponent( "javax.swing.JButton" ) {
+ name: "btnClose"
+ "text": "Zusammenführen"
+ auxiliary() {
+ "JavaCodeGenerator.variableName": "btnMerge"
+ }
+ }, new FormLayoutConstraints( class com.jformdesigner.runtime.GridBagConstraintsEx ) {
+ "gridy": 0
+ "gridx": 2
+ } )
+ }, new FormLayoutConstraints( class java.lang.String ) {
+ "value": "South"
+ } )
+ }, new FormLayoutConstraints( class java.lang.String ) {
+ "value": "Center"
+ } )
+ }, new FormLayoutConstraints( null ) {
+ "location": new java.awt.Point( 0, 0 )
+ "size": new java.awt.Dimension( 555, 295 )
+ } )
+ }
+}
diff --git a/src/main/java/mediathek/gui/tabs/tab_downloads/GuiDownloads.java b/src/main/java/mediathek/gui/tabs/tab_downloads/GuiDownloads.java
index dc33870893..4a8e9f9c1a 100644
--- a/src/main/java/mediathek/gui/tabs/tab_downloads/GuiDownloads.java
+++ b/src/main/java/mediathek/gui/tabs/tab_downloads/GuiDownloads.java
@@ -106,6 +106,7 @@ public class GuiDownloads extends AGuiTabPanel {
protected DeleteDownloadAction deleteDownloadAction = new DeleteDownloadAction(this);
protected OpenTargetFolderAction openTargetFolderAction = new OpenTargetFolderAction(this);
protected ToggleFilterPanelAction toggleFilterPanelAction = new ToggleFilterPanelAction();
+ protected MergeSubtitleWithVideoAction mergeSubtitleWithVideoAction = new MergeSubtitleWithVideoAction(MediathekGui.ui());
protected JToolBar swingToolBar = new JToolBar();
private boolean onlyAbos;
private boolean onlyDownloads;
@@ -341,6 +342,8 @@ public void installMenuEntries(JMenu menu) {
menu.add(deleteDownloadsAction);
menu.add(editDownloadAction);
menu.addSeparator();
+ menu.add(mergeSubtitleWithVideoAction);
+ menu.addSeparator();
menu.add(cbShowDownloadDescription);
menu.addSeparator();
menu.add(miMarkFilmAsSeen);
diff --git a/src/main/java/mediathek/tool/ColorUtils.java b/src/main/java/mediathek/tool/ColorUtils.java
index 981b08c2c0..7e32cd4f59 100644
--- a/src/main/java/mediathek/tool/ColorUtils.java
+++ b/src/main/java/mediathek/tool/ColorUtils.java
@@ -1,3 +1,21 @@
+/*
+ * Copyright (c) 2024 derreisende77.
+ * This code was developed as part of the MediathekView project https://github.com/mediathekview/MediathekView
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
package mediathek.tool;
import java.awt.*;
diff --git a/src/main/java/mediathek/tool/DarkModeDetector.java b/src/main/java/mediathek/tool/DarkModeDetector.java
index 31d0de6af1..415b63d889 100644
--- a/src/main/java/mediathek/tool/DarkModeDetector.java
+++ b/src/main/java/mediathek/tool/DarkModeDetector.java
@@ -1,3 +1,21 @@
+/*
+ * Copyright (c) 2024 derreisende77.
+ * This code was developed as part of the MediathekView project https://github.com/mediathekview/MediathekView
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
package mediathek.tool;
import org.apache.commons.lang3.SystemUtils;
diff --git a/src/main/java/mediathek/tool/FileDialogs.kt b/src/main/java/mediathek/tool/FileDialogs.kt
index 62d5b6c7b8..7b7e49c025 100644
--- a/src/main/java/mediathek/tool/FileDialogs.kt
+++ b/src/main/java/mediathek/tool/FileDialogs.kt
@@ -4,6 +4,7 @@ import org.apache.commons.lang3.SystemUtils
import java.awt.FileDialog
import java.awt.Frame
import java.io.File
+import javax.swing.JDialog
import javax.swing.JFileChooser
class FileDialogs {
@@ -44,6 +45,40 @@ class FileDialogs {
return resultFile
}
+ @JvmStatic
+ fun chooseLoadFileLocation(parent: JDialog, title: String, initialFile: String): File? {
+ var resultFile: File? = null
+
+ if (SystemUtils.IS_OS_MAC_OSX || SystemUtils.IS_OS_WINDOWS) {
+ val chooser = FileDialog(parent, title)
+ chooser.mode = FileDialog.LOAD
+ chooser.isMultipleMode = false
+ if (initialFile.isNotEmpty()) {
+ chooser.directory = initialFile
+ }
+ chooser.isVisible = true
+ if (chooser.file != null) {
+ val files = chooser.files
+ if (files.isNotEmpty()) {
+ resultFile = files[0]
+ }
+ }
+ } else {
+ val chooser = JFileChooser()
+ if (initialFile.isNotEmpty()) {
+ chooser.currentDirectory = File(initialFile)
+ }
+ chooser.fileSelectionMode = JFileChooser.FILES_ONLY
+ chooser.dialogTitle = title
+ chooser.isFileHidingEnabled = true
+ if (chooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) {
+ resultFile = File(chooser.selectedFile.absolutePath)
+ }
+ }
+
+ return resultFile
+ }
+
@JvmStatic
fun chooseLoadFileLocation(parent: Frame, title: String, initialFile: String): File? {
var resultFile: File? = null
@@ -117,5 +152,38 @@ class FileDialogs {
}
return resultFile
}
+
+ @JvmStatic
+ fun chooseSaveFileLocation(parent: JDialog, title: String, initialFile: String): File? {
+ var resultFile: File? = null
+ if (SystemUtils.IS_OS_MAC_OSX || SystemUtils.IS_OS_WINDOWS) {
+ val chooser = FileDialog(parent, title)
+ chooser.mode = FileDialog.SAVE
+ chooser.isMultipleMode = false
+ if (initialFile.isNotEmpty()) {
+ chooser.directory = initialFile
+ }
+ chooser.isVisible = true
+ if (chooser.file != null) {
+ val files = chooser.files
+ if (files.isNotEmpty()) {
+ resultFile = files[0]
+ }
+ }
+ } else {
+ //Linux HiDPI does not work with either AWT FileDialog or JavaFX FileChooser as of JFX 14.0.1
+ val chooser = JFileChooser()
+ if (initialFile.isNotEmpty()) {
+ chooser.currentDirectory = File(initialFile)
+ }
+ chooser.fileSelectionMode = JFileChooser.FILES_ONLY
+ chooser.dialogTitle = title
+ chooser.isFileHidingEnabled = true
+ if (chooser.showSaveDialog(parent) == JFileChooser.APPROVE_OPTION) {
+ resultFile = File(chooser.selectedFile.absolutePath)
+ }
+ }
+ return resultFile
+ }
}
}
\ No newline at end of file
diff --git a/src/main/java/mediathek/tool/LanguageCode.java b/src/main/java/mediathek/tool/LanguageCode.java
new file mode 100644
index 0000000000..e9350f17fd
--- /dev/null
+++ b/src/main/java/mediathek/tool/LanguageCode.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (c) 2024 derreisende77.
+ * This code was developed as part of the MediathekView project https://github.com/mediathekview/MediathekView
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package mediathek.tool;
+
+import com.ibm.icu.util.ULocale;
+import org.jetbrains.annotations.NotNull;
+
+public enum LanguageCode {
+ aa("Afar", "Afaraf"),
+ ab("Abkhazian", "Аҧсуа"),
+ ae("Avestan", "Avesta"),
+ af("Afrikaans", "Afrikaans"),
+ ak("Akan", "Akan"),
+ am("Amharic", "አማርኛ"),
+ an("Aragonese", "Aragonés"),
+ ar("Arabic", "العربية"),
+ av("Avaric", "авар мацӀ; магӀарул мацӀ"),
+ ay("Aymara", "aymar aru"),
+ az("Azerbaijani", "azərbaycan dili"),
+ ba("Bashkir", "башҡорт теле"),
+ be("Belarusian", "Беларуская"),
+ bi("Bislama", "Bislama"),
+ bg("Bulgarian", "български език"),
+ //bh("Bihari", "भोजपुरी"),
+ bm("Bambara", "bamanankan"),
+ bn("Bengali", "বাংলা"),
+ bo("Tibetan", "བོད་ཡིག"),
+ br("Breton", "brezhoneg"),
+ bs("Bosnian", "bosanski jezik"),
+ ca("Catalan", "Català"),
+ ch("Chamorro", "Chamoru"),
+ co("Corsican", "corsu; lingua corsa"),
+ cr("Cree", "ᓀᐦᐃᔭᐍᐏᐣ"),
+ cs("Czech", "česky; čeština"),
+ cu("Church Slavic", "ѩзыкъ словѣньскъ"),
+ cv("Chuvash", "чӑваш чӗлхи"),
+ cy("Welsh", "Cymraeg"),
+ da("Danish", "Dansk"),
+ de("German", "Deutsch"),
+ dv("Divehi", "ދިވެހި"),
+ dz("Dzongkha", "རྫོང་ཁ"),
+ ee("Ewe", "Ɛʋɛgbɛ"),
+ el("Greek", "Ελληνικά"),
+ en("English", "English"),
+ eo("Esperanto", "Esperanto"),
+ es("Spanish", "Español"),
+ et("Estonian", "eesti; eesti keel"),
+ eu("Basque", "euskara; euskera"),
+ fa("Persian", "فارسی"),
+ ff("Fulah", "Fulfulde"),
+ fi("Finnish", "suomi; suomen kieli"),
+ fj("Fijian", "vosa Vakaviti"),
+ fo("Faroese", "Føroyskt"),
+ fr("French", "Français"),
+ fy("Western Frisian", "Frysk"),
+ ga("Irish", "Gaeilge"),
+ gd("Scottish Gaelic", "Gàidhlig"),
+ gl("Galician", "Galego"),
+ gn("Guaraní", "Avañe'ẽ"),
+ gu("Gujarati", "ગુજરાતી"),
+ gv("Manx", "Gaelg, Gailck"),
+ ha("Hausa", "هَوُسَ"),
+ he("Hebrew", "עברית"),
+ hi("Hindi", "हिन्दी; हिंदी"),
+ ho("Hiri Motu", "Hiri Motu"),
+ hr("Croatian", "Hrvatski"),
+ ht("Haitian", "Kreyòl ayisyen"),
+ hu("Hungarian", "Magyar"),
+ hy("Armenian", "Հայերեն"),
+ hz("Herero", "Otjiherero"),
+ ia("Interlingua (International Auxiliary Language Association)", "Interlingua"),
+ id("Indonesian", "Bahasa Indonesia"),
+ ie("Interlingue", "Interlingue"),
+ ig("Igbo", "Igbo"),
+ ii("Sichuan Yi", "ꆇꉙ"),
+ ik("Inupiaq", "Iñupiaq; Iñupiatun"),
+ io("Ido", "Ido"),
+ is("Icelandic", "Íslenska"),
+ it("Italian", "Italiano"),
+ iu("Inuktitut", "ᐃᓄᒃᑎᑐᑦ"),
+ ja("Japanese", "Nihongo"),
+ ka("Georgian", "ქართული"),
+ kg("Kongo", "KiKongo"),
+ ki("Kikuyu", "Gĩkũyũ"),
+ kj("Kwanyama", "Kuanyama"),
+ ku("Kurdish", "كوردی"),
+ kk("Kazakh", "Қазақ тілі"),
+ kl("Kalaallisut", "kalaallisut; kalaallit oqaasii"),
+ km("Khmer", "ភាសាខ្មែរ"),
+ kn("Kannada", "ಕನ್ನಡ"),
+ ko("Korean", "Kanuri"),
+ ks("Kashmiri", "коми кыв"),
+ kw("Cornish", "Kernewek"),
+ ky("Kirghiz", "кыргыз тили"),
+ la("Latin", "Latine; lingua latina"),
+ lb("Luxembourgish", "Lëtzebuergesch"),
+ lg("Ganda", "Luganda"),
+ li("Limburgish", "Limburgs"),
+ ln("Lingala", "Lingála"),
+ lo("Lao", "ພາສາລາວ"),
+ lt("Lithuanian", "Lietuvių kalba"),
+ lu("Luba-Katanga", "Latviešu valoda"),
+ mg("Malagasy", "Malagasy fiteny"),
+ mh("Marshallese", "Kajin M̧ajeļ"),
+ mi("Māori", "Te reo Māori"),
+ mk("Macedonian", "македонски јазик"),
+ ml("Malayalam", "മലയാളം"),
+ mn("Mongolian", "Монгол"),
+ mr("Marathi", "मराठी"),
+ ms("Malay", "بهاس ملايو"),
+ mt("Maltese", "Malti"),
+ my("Burmese", "ဗမာစာ"),
+ na("Nauru", "Ekakairũ Naoero"),
+ nb("Norwegian Bokmål", "Norsk bokmål"),
+ nd("North Ndebele", "isiNdebele"),
+ ne("Nepali", "नेपाली"),
+ ng("Ndonga", "Owambo"),
+ nl("Dutch", "Nederlands"),
+ nn("Norwegian Nynorsk", "Norsk nynorsk"),
+ no("Norwegian", "Norsk"),
+ nr("South Ndebele", "isiNdebele"),
+ nv("Navajo", "Diné bizaad; Dinékʼehǰí"),
+ ny("Chichewa", "chiCheŵa; chinyanja"),
+ oc("Occitan", "Occitan"),
+ oj("Ojibwa", "ᐊᓂᔑᓈᐯᒧᐎᓐ"),
+ om("Oromo", "Afaan Oromoo"),
+ or("Oriya", "ଓଡ଼ିଆ"),
+ os("Ossetian", "Ирон æвзаг"),
+ pa("Panjabi", "पाऴि"),
+ pl("Polish", "Polski"),
+ ps("Pashto", "پښتو"),
+ pt("Portuguese", "Português"),
+ qu("Quechua", "Runa Simi; Kichwa"),
+ rm("Raeto-Romance", "rumantsch grischun"),
+ rn("Kirundi", "kiRundi"),
+ ro("Romanian", "Română"),
+ ru("Russian", "русский язык"),
+ rw("Kinyarwanda", "Ikinyarwanda"),
+ sa("Sanskrit", "संस्कृतम्"),
+ sc("Sardinian", "sardu"),
+ sd("Sindhi", "Davvisámegiella"),
+ sg("Sango", "yângâ tî sängö"),
+ si("Sinhala", "සිංහල"),
+ sk("Slovak", "Slovenčina"),
+ sl("Slovenian", "Slovenščina"),
+ sm("Samoan", "gagana fa'a Samoa"),
+ sn("Shona", "chiShona"),
+ so("Somali", "Soomaaliga; af Soomaali"),
+ sq("Albanian", "Shqip"),
+ sr("Serbian", "српски језик"),
+ ss("Swati", "SiSwati"),
+ st("Southern Sotho", "Sesotho"),
+ su("Sundanese", "Basa Sunda"),
+ sv("Swedish", "svenska"),
+ sw("Swahili", "Kiswahili"),
+ ta("Tamil", "தமிழ்"),
+ te("Telugu", "తెలుగు"),
+ tg("Tajik", "ไทย"),
+ ti("Tigrinya", "ትግርኛ"),
+ tk("Turkmen", "Türkmen; Түркмен"),
+ //tl("Tagalog", "Tagalog"),
+ tn("Tswana", "Setswana"),
+ to("Tonga", "faka Tonga"),
+ tr("Turkish", "Türkçe"),
+ ts("Tsonga", "Xitsonga"),
+ tt("Tatar", "Twi"),
+ ty("Tahitian", "Reo Mā`ohi"),
+ ug("Uighur", "Українська"),
+ ur("Urdu", "اردو"),
+ uk("Ukrainian", "Ukraïna"),
+ uz("Uzbek", "Tshivenḓa"),
+ vi("Vietnamese", "Tiếng Việt"),
+ vo("Volapük", "Volapük"),
+ wa("Walloon", "Walon"),
+ wo("Wolof", "Wollof"),
+ xh("Xhosa", "isiXhosa"),
+ yi("Yiddish", "ייִדיש"),
+ yo("Yoruba", "Yorùbá"),
+ za("Zhuang", "Saɯ cueŋƅ; Saw cuengh"),
+ zh("Chinese", "中文 (Zhōngwén), 汉语, 漢語"),
+ zu("Zulu", "isiZulu");
+
+ private final String readableName;
+ private final String nativeName;
+ LanguageCode(String readableName, String nativeName) {
+ this.readableName = readableName;
+ this.nativeName = nativeName;
+ }
+ public String readableName() { return readableName;}
+ public String nativeName() { return nativeName;}
+ public @NotNull String getISO3Language() throws IllegalArgumentException {
+ ULocale locale = new ULocale(this.name());
+ var isocode = locale.getISO3Language();
+ if (isocode.isEmpty())
+ throw new IllegalArgumentException("Language code '" + this.name() + "' is empty");
+ else return isocode;
+ }
+
+ public static LanguageCode fromNativeName(@NotNull String nativeName) throws IllegalArgumentException {
+ for (var item: LanguageCode.values()) {
+ if (item.nativeName.equals(nativeName))
+ return item;
+ }
+ throw new IllegalArgumentException("Language code '" + nativeName + "' not found");
+ }
+}
diff --git a/src/main/java/mediathek/tool/SwingErrorDialog.java b/src/main/java/mediathek/tool/SwingErrorDialog.java
index 79d35dfd0c..8c1ddd4027 100644
--- a/src/main/java/mediathek/tool/SwingErrorDialog.java
+++ b/src/main/java/mediathek/tool/SwingErrorDialog.java
@@ -12,7 +12,7 @@
public class SwingErrorDialog {
public static void showExceptionMessage(@Nullable Component parentComponent,
@NotNull String messageText,
- @NotNull Exception exception) throws HeadlessException {
+ @NotNull Throwable exception) throws HeadlessException {
StringWriter stringWriter = new StringWriter();
exception.printStackTrace(new PrintWriter(stringWriter));
diff --git a/src/test/java/mediathek/tool/LanguageCodeTest.java b/src/test/java/mediathek/tool/LanguageCodeTest.java
new file mode 100644
index 0000000000..26351c24d7
--- /dev/null
+++ b/src/test/java/mediathek/tool/LanguageCodeTest.java
@@ -0,0 +1,21 @@
+package mediathek.tool;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.EnumSet;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+class LanguageCodeTest {
+ /**
+ * Test if all codes can be converted to 3 letter code and there is no exception.
+ */
+ @Test
+ public void testConversion() {
+ for (var code : EnumSet.allOf(LanguageCode.class)) {
+ var out = code.getISO3Language();
+ assertFalse(out.isEmpty());
+ }
+
+ }
+}
\ No newline at end of file