Compare commits
41 Commits
advanced-a
...
master
Author | SHA1 | Date | |
---|---|---|---|
4fdbfb28e3 | |||
87f5743a24 | |||
972691db76 | |||
2145bfb8ea | |||
c0249c3b3a | |||
da67b195de | |||
5238697c70 | |||
32be7d0aec | |||
1ceb378757 | |||
1dc489a6f8 | |||
c0c8c9c054 | |||
c3c89fcb75 | |||
380a1b800a | |||
ac25ca1986 | |||
dae93b9f81 | |||
d46f12e690 | |||
92b46bdc9e | |||
ded88eb5b5 | |||
3c9fa55585 | |||
d487df0e78 | |||
4ebd29b358 | |||
f0e75eb440 | |||
461c7552b3 | |||
376d5655f2 | |||
be88845731 | |||
2c75d91cce | |||
2962114601 | |||
692d9e79d2 | |||
6c614b2f17 | |||
346a5e0606 | |||
2346e651ef | |||
f81a21b9e9 | |||
56f5e31934 | |||
5d94cabca0 | |||
a9ea1f796a | |||
32ec50ba7d | |||
3c298f623e | |||
1323513e46 | |||
388563574f | |||
750498810c | |||
c9bd648437 |
@ -1,3 +0,0 @@
|
||||
Manifest-Version: 1.0
|
||||
Main-Class: net.knarcraft.ffmpegconverter.Main
|
||||
|
@ -1,3 +0,0 @@
|
||||
Manifest-Version: 1.0
|
||||
X-COMMENT: Main-Class will be added automatically by build
|
||||
|
74
pom.xml
74
pom.xml
@ -6,7 +6,6 @@
|
||||
<groupId>net.knarcraft.ffmpegconvert</groupId>
|
||||
<artifactId>ffmpegconvert</artifactId>
|
||||
<version>0.1-alpha</version>
|
||||
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>FFMpeg Convert</name>
|
||||
@ -33,8 +32,7 @@
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
<java.version>16</java.version>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
@ -64,44 +62,76 @@
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<!-- Build an executable JAR -->
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.2.0</version>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<configuration>
|
||||
<source>${java.version}</source>
|
||||
<target>${java.version}</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>bundled</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
<archive>
|
||||
<manifest>
|
||||
<addClasspath>true</addClasspath>
|
||||
<classpathPrefix>lib/</classpathPrefix>
|
||||
<mainClass>net.knarcraft.ffmpegconverter.Main</mainClass>
|
||||
<mainClass>net.knarcraft.ffmpegconverter.FFMpegConvert</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
<directory>${project.basedir}/target</directory>
|
||||
<outputDirectory>${project.build.directory}/classes</outputDirectory>
|
||||
<finalName>${project.artifactId}-${project.version}</finalName>
|
||||
<testOutputDirectory>${project.build.directory}/test-classes</testOutputDirectory>
|
||||
<sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>
|
||||
<testSourceDirectory>${project.basedir}/src/test/java</testSourceDirectory>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>${project.basedir}/src/main/resources</directory>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
</resource>
|
||||
</resources>
|
||||
<testResources>
|
||||
<testResource>
|
||||
<directory>${project.basedir}/src/test/resources</directory>
|
||||
</testResource>
|
||||
</testResources>
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.11</version>
|
||||
<version>4.13.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains</groupId>
|
||||
<artifactId>annotations</artifactId>
|
||||
<version>20.1.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-configuration2</artifactId>
|
||||
<version>2.10.1</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-beanutils</groupId>
|
||||
<artifactId>commons-beanutils</artifactId>
|
||||
<version>1.9.4</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>5.10.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
383
src/main/java/net/knarcraft/ffmpegconverter/FFMpegConvert.java
Normal file
383
src/main/java/net/knarcraft/ffmpegconverter/FFMpegConvert.java
Normal file
@ -0,0 +1,383 @@
|
||||
package net.knarcraft.ffmpegconverter;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.config.Configuration;
|
||||
import net.knarcraft.ffmpegconverter.converter.AnimeConverter;
|
||||
import net.knarcraft.ffmpegconverter.converter.AudioConverter;
|
||||
import net.knarcraft.ffmpegconverter.converter.AudioExtractor;
|
||||
import net.knarcraft.ffmpegconverter.converter.AudioToVorbisConverter;
|
||||
import net.knarcraft.ffmpegconverter.converter.Converter;
|
||||
import net.knarcraft.ffmpegconverter.converter.DownScaleConverter;
|
||||
import net.knarcraft.ffmpegconverter.converter.LetterboxCropper;
|
||||
import net.knarcraft.ffmpegconverter.converter.MKVToMP4Transcoder;
|
||||
import net.knarcraft.ffmpegconverter.converter.MkvH264Converter;
|
||||
import net.knarcraft.ffmpegconverter.converter.MkvH265ReducedConverter;
|
||||
import net.knarcraft.ffmpegconverter.converter.StreamOrderConverter;
|
||||
import net.knarcraft.ffmpegconverter.converter.SubtitleEmbed;
|
||||
import net.knarcraft.ffmpegconverter.converter.VideoConverter;
|
||||
import net.knarcraft.ffmpegconverter.converter.WebAnimeConverter;
|
||||
import net.knarcraft.ffmpegconverter.converter.WebVideoConverter;
|
||||
import net.knarcraft.ffmpegconverter.property.MinimalSubtitlePreference;
|
||||
import net.knarcraft.ffmpegconverter.utility.FileUtil;
|
||||
import net.knarcraft.ffmpegconverter.utility.OutputUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Scanner;
|
||||
|
||||
import static net.knarcraft.ffmpegconverter.utility.Parser.tokenize;
|
||||
|
||||
/**
|
||||
* The main class for starting the software
|
||||
*/
|
||||
public class FFMpegConvert {
|
||||
|
||||
private static final String FFPROBE_PATH = "ffprobe"; //Can be just ffprobe if it's in the path
|
||||
private static final String FFMPEG_PATH = "ffmpeg"; //Can be just ffmpeg if it's in the path
|
||||
private static final Scanner READER = new Scanner(System.in, StandardCharsets.UTF_8);
|
||||
private static Converter converter = null;
|
||||
private static final Configuration configuration = new Configuration();
|
||||
|
||||
public static void main(@NotNull String[] arguments) throws IOException {
|
||||
OutputUtil.setDebug(configuration.isDebugEnabled());
|
||||
|
||||
converter = loadConverter();
|
||||
|
||||
if (converter == null) {
|
||||
System.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> input;
|
||||
do {
|
||||
OutputUtil.println("<Folder/File> [Recursions]:");
|
||||
input = readInput(2);
|
||||
} while (input.isEmpty());
|
||||
File folder = new File(input.get(0));
|
||||
|
||||
int recursionSteps = 1;
|
||||
if (input.size() > 1) {
|
||||
try {
|
||||
recursionSteps = Integer.parseInt(input.get(1));
|
||||
} catch (NumberFormatException e) {
|
||||
OutputUtil.println("Recursion steps is invalid and will be ignored.");
|
||||
}
|
||||
}
|
||||
|
||||
convertAllFiles(folder, recursionSteps);
|
||||
OutputUtil.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the configuration handler
|
||||
*
|
||||
* @return <p>The configuration handler</p>
|
||||
*/
|
||||
@NotNull
|
||||
public static Configuration getConfiguration() {
|
||||
return configuration;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Asks the user which converter they want, and assigns a converter instance to the converter variable
|
||||
*/
|
||||
@Nullable
|
||||
private static Converter loadConverter() {
|
||||
int choice = getChoice("""
|
||||
Which converter do you want do use?
|
||||
1. Anime to web mp4
|
||||
2. Audio converter
|
||||
3. Video converter
|
||||
4. Web video converter
|
||||
5. MKV to h264 converter
|
||||
6. MKV to h265 reduced converter
|
||||
7. MKV to MP4 transcoder
|
||||
8. DownScaleConverter
|
||||
9. mp4 Subtitle Embed
|
||||
10. Anime to h265 all streams
|
||||
11. Stream reorder
|
||||
12. Letterbox cropper
|
||||
13. Video's Audio to vorbis converter
|
||||
14. Audio from video extractor""", 1, 14, Integer.MIN_VALUE);
|
||||
|
||||
return switch (choice) {
|
||||
case 1 -> generateWebAnimeConverter();
|
||||
case 2 -> new AudioConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output audio extension>", null));
|
||||
case 3 -> new VideoConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output video extension>", null));
|
||||
case 4 -> new WebVideoConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output video extension>", null));
|
||||
case 5 -> new MkvH264Converter(FFPROBE_PATH, FFMPEG_PATH);
|
||||
case 6 -> new MkvH265ReducedConverter(FFPROBE_PATH, FFMPEG_PATH);
|
||||
case 7 -> generateMKVToMP4Transcoder();
|
||||
case 8 -> generateDownScaleConverter();
|
||||
case 9 -> new SubtitleEmbed(FFPROBE_PATH, FFMPEG_PATH);
|
||||
case 10 -> generateAnimeConverter();
|
||||
case 11 -> generateStreamOrderConverter();
|
||||
case 12 -> new LetterboxCropper(FFPROBE_PATH, FFMPEG_PATH);
|
||||
case 13 -> new AudioToVorbisConverter(FFPROBE_PATH, FFMPEG_PATH);
|
||||
case 14 -> new AudioExtractor(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output audio extension>",
|
||||
"mp3"), getChoice("<stream to extract>", 0, 1000, 0));
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the file(s) as specified
|
||||
*
|
||||
* @param fileOrFolder <p>A file or a folder.</p>
|
||||
* @param recursionSteps <p>The depth to recurse if a folder is given.</p>
|
||||
* @throws IOException <p>If conversion or writing fails.</p>
|
||||
*/
|
||||
private static void convertAllFiles(@NotNull File fileOrFolder, int recursionSteps) throws IOException {
|
||||
if (fileOrFolder.isDirectory()) {
|
||||
File[] files = FileUtil.listFilesRecursive(fileOrFolder, converter.getValidFormats(), recursionSteps);
|
||||
if (files != null && files.length > 0) {
|
||||
for (File file : files) {
|
||||
converter.convert(file);
|
||||
}
|
||||
} else {
|
||||
OutputUtil.println("No valid files found in folder.");
|
||||
}
|
||||
} else if (fileOrFolder.exists()) {
|
||||
String path = fileOrFolder.getPath();
|
||||
if (converter.getValidFormats().stream().anyMatch((format) -> format.equalsIgnoreCase(
|
||||
path.substring(path.lastIndexOf('.') + 1)))) {
|
||||
converter.convert(fileOrFolder);
|
||||
} else {
|
||||
OutputUtil.println("The specified file " + fileOrFolder.getAbsolutePath() + " is not supported for " +
|
||||
"the selected converter.");
|
||||
}
|
||||
} else {
|
||||
OutputUtil.println("Path " + fileOrFolder.getAbsolutePath() + " does not point to any file or folder.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes and returns the downscale converter
|
||||
*
|
||||
* @return <p>The initialized downscale converter</p>
|
||||
*/
|
||||
@Nullable
|
||||
private static Converter generateDownScaleConverter() {
|
||||
OutputUtil.println("(New width e.x. 1920) (New height e.x. 1080)\nYour input: ");
|
||||
List<String> input = readInput(3);
|
||||
int newWidth;
|
||||
int newHeight;
|
||||
|
||||
try {
|
||||
newWidth = Integer.parseInt(input.get(0));
|
||||
newHeight = Integer.parseInt(input.get(1));
|
||||
return new DownScaleConverter(FFPROBE_PATH, FFMPEG_PATH, newWidth, newHeight);
|
||||
} catch (NumberFormatException exception) {
|
||||
OutputUtil.println("Width or height is not a number");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes and returns the MKV to MP4 transcoder
|
||||
*
|
||||
* @return <p>The initialized transcoder</p>
|
||||
*/
|
||||
@Nullable
|
||||
private static Converter generateMKVToMP4Transcoder() {
|
||||
OutputUtil.println("[Audio stream index 0-n] [Subtitle stream index 0-n] [Video stream index 0-n]\nYour input: ");
|
||||
List<String> input = readInput(3);
|
||||
int audioStreamIndex = 0;
|
||||
int subtitleStreamIndex = 0;
|
||||
int videoStreamIndex = 0;
|
||||
|
||||
try {
|
||||
if (!input.isEmpty()) {
|
||||
audioStreamIndex = Integer.parseInt(input.get(0));
|
||||
}
|
||||
if (input.size() > 1) {
|
||||
subtitleStreamIndex = Integer.parseInt(input.get(1));
|
||||
}
|
||||
if (input.size() > 2) {
|
||||
videoStreamIndex = Integer.parseInt(input.get(2));
|
||||
}
|
||||
return new MKVToMP4Transcoder(FFPROBE_PATH, FFMPEG_PATH, audioStreamIndex, subtitleStreamIndex, videoStreamIndex);
|
||||
} catch (NumberFormatException exception) {
|
||||
OutputUtil.println("Audio, Subtitle or Video stream index is not a number");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes and returns a stream reorder converter
|
||||
*
|
||||
* @return <p>The initialized stream order converter</p>
|
||||
*/
|
||||
@Nullable
|
||||
private static Converter generateStreamOrderConverter() {
|
||||
OutputUtil.println("Note that a * in the sort order matches any stream not yet matched, and 0 matches " +
|
||||
"undefined language streams. The subtitle name filter will include any streams with titles " +
|
||||
"containing the filter, but if it contains RegEx expressions, a RegEx match will be performed " +
|
||||
"instead.\nYour input: ");
|
||||
OutputUtil.println("<Audio sort order> <Subtitle sort order> [Subtitle name filter]");
|
||||
List<String> input = readInput(3);
|
||||
|
||||
if (input.size() < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String audioLanguages = input.get(0);
|
||||
String subtitleLanguages = input.get(1);
|
||||
String subtitleNameFilter = "*";
|
||||
if (input.size() > 2) {
|
||||
subtitleNameFilter = input.get(2);
|
||||
}
|
||||
|
||||
return new StreamOrderConverter(FFPROBE_PATH, FFMPEG_PATH, audioLanguages, subtitleLanguages, subtitleNameFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes and returns the anime converter
|
||||
*
|
||||
* @return <p>The initialized anime converter</p>
|
||||
*/
|
||||
@Nullable
|
||||
private static Converter generateAnimeConverter() {
|
||||
OutputUtil.println("[Forced audio index 0-n] [Forced subtitle index 0-n] [Force video encoding true/false] " +
|
||||
"[Force audio encoding true/false] [Subtitle name filter]\nYour input: ");
|
||||
List<String> input = readInput(5);
|
||||
int forcedAudioIndex = 0;
|
||||
int forcedSubtitleIndex = 0;
|
||||
String subtitleNameFilter = "";
|
||||
boolean forceVideoEncoding = false;
|
||||
boolean forceAudioEncoding = false;
|
||||
|
||||
try {
|
||||
if (!input.isEmpty()) {
|
||||
forcedAudioIndex = Integer.parseInt(input.get(0));
|
||||
}
|
||||
if (input.size() > 1) {
|
||||
forcedSubtitleIndex = Integer.parseInt(input.get(1));
|
||||
}
|
||||
} catch (NumberFormatException exception) {
|
||||
OutputUtil.println("Forced audio or subtitle index is not a number");
|
||||
return null;
|
||||
}
|
||||
if (input.size() > 2) {
|
||||
forceVideoEncoding = Boolean.parseBoolean(input.get(2));
|
||||
}
|
||||
if (input.size() > 3) {
|
||||
forceAudioEncoding = Boolean.parseBoolean(input.get(3));
|
||||
}
|
||||
if (input.size() > 4) {
|
||||
subtitleNameFilter = input.get(4);
|
||||
}
|
||||
return new AnimeConverter(FFPROBE_PATH, FFMPEG_PATH, forcedAudioIndex, forcedSubtitleIndex, subtitleNameFilter,
|
||||
forceVideoEncoding, forceAudioEncoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes and returns the web anime converter
|
||||
*
|
||||
* @return <p>The initialized anime converter</p>
|
||||
*/
|
||||
@Nullable
|
||||
private static Converter generateWebAnimeConverter() {
|
||||
OutputUtil.println("[Convert to stereo if necessary true/false] [Prevent signs&songs subtitles true/false] [Forced audio index 0-n] " +
|
||||
"[Forced subtitle index 0-n] [Subtitle name filter]\nYour input: ");
|
||||
List<String> input = readInput(5);
|
||||
boolean toStereo = true;
|
||||
MinimalSubtitlePreference subtitlePreference = MinimalSubtitlePreference.AVOID;
|
||||
int forcedAudioIndex = 0;
|
||||
int forcedSubtitleIndex = 0;
|
||||
String subtitleNameFilter = "";
|
||||
|
||||
if (!input.isEmpty()) {
|
||||
toStereo = Boolean.parseBoolean(input.get(0));
|
||||
}
|
||||
if (input.size() > 1) {
|
||||
subtitlePreference = MinimalSubtitlePreference.valueOf(input.get(1).toUpperCase());
|
||||
}
|
||||
try {
|
||||
if (input.size() > 2) {
|
||||
forcedAudioIndex = Integer.parseInt(input.get(2));
|
||||
}
|
||||
if (input.size() > 3) {
|
||||
forcedSubtitleIndex = Integer.parseInt(input.get(3));
|
||||
}
|
||||
} catch (NumberFormatException exception) {
|
||||
OutputUtil.println("Forced audio or subtitle index is not a number");
|
||||
return null;
|
||||
}
|
||||
if (input.size() > 4) {
|
||||
subtitleNameFilter = input.get(4);
|
||||
}
|
||||
return new WebAnimeConverter(FFPROBE_PATH, FFMPEG_PATH, toStereo,
|
||||
subtitlePreference, forcedAudioIndex, forcedSubtitleIndex, subtitleNameFilter);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reads a number of tokens from the user input
|
||||
*
|
||||
* @param max <p>The number of tokens expected.</p>
|
||||
* @return <p>A list of tokens.</p>
|
||||
*/
|
||||
@NotNull
|
||||
private static List<String> readInput(int max) {
|
||||
List<String> tokens = tokenize(READER.nextLine());
|
||||
if (max < tokens.size()) {
|
||||
throw new IllegalArgumentException("Input contains " + tokens.size() +
|
||||
" arguments, but the input only supports " + max + " arguments.");
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user's choice
|
||||
*
|
||||
* @param prompt <p>The prompt shown to the user.</p>
|
||||
* @param defaultValue <p>The default value, if no input is given</p>
|
||||
* @return <p>The non-empty choice given by the user.</p>
|
||||
*/
|
||||
@NotNull
|
||||
private static String getChoice(@NotNull String prompt, @Nullable Object defaultValue) {
|
||||
OutputUtil.println(prompt);
|
||||
String choice = "";
|
||||
while (choice.isEmpty()) {
|
||||
OutputUtil.println("Your input: ");
|
||||
choice = READER.nextLine();
|
||||
if (choice.isEmpty() && defaultValue != null) {
|
||||
return String.valueOf(defaultValue);
|
||||
}
|
||||
}
|
||||
return choice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an integer from the user
|
||||
*
|
||||
* @param prompt The prompt to give the user
|
||||
* @param min The minimum allowed value
|
||||
* @param max The maximum allowed value
|
||||
* @return The value given by the user
|
||||
*/
|
||||
private static int getChoice(@NotNull String prompt, int min, int max, int defaultValue) {
|
||||
OutputUtil.println(prompt);
|
||||
int choice = Integer.MIN_VALUE;
|
||||
do {
|
||||
OutputUtil.println("Your input: ");
|
||||
try {
|
||||
choice = Integer.parseInt(READER.next());
|
||||
} catch (NumberFormatException e) {
|
||||
if (defaultValue != Integer.MIN_VALUE) {
|
||||
return defaultValue;
|
||||
}
|
||||
OutputUtil.println("Invalid choice. Please try again.");
|
||||
} finally {
|
||||
READER.nextLine();
|
||||
}
|
||||
} while (choice < min || choice > max);
|
||||
return choice;
|
||||
}
|
||||
|
||||
}
|
@ -1,187 +0,0 @@
|
||||
package net.knarcraft.ffmpegconverter;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.converter.AbstractConverter;
|
||||
import net.knarcraft.ffmpegconverter.converter.AnimeConverter;
|
||||
import net.knarcraft.ffmpegconverter.converter.AudioConverter;
|
||||
import net.knarcraft.ffmpegconverter.converter.VideoConverter;
|
||||
import net.knarcraft.ffmpegconverter.converter.WebVideoConverter;
|
||||
import net.knarcraft.ffmpegconverter.utility.FileUtil;
|
||||
import net.knarcraft.ffmpegconverter.utility.ListUtil;
|
||||
import net.knarcraft.ffmpegconverter.utility.OutputUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Scanner;
|
||||
|
||||
import static net.knarcraft.ffmpegconverter.utility.Parser.tokenize;
|
||||
|
||||
/**
|
||||
* The main class for starting the software
|
||||
*/
|
||||
class Main {
|
||||
private static final String FFPROBE_PATH = "ffprobe"; //Can be just ffprobe if it's in the path
|
||||
private static final String FFMPEG_PATH = "ffmpeg"; //Can be just ffmpeg if it's in the path
|
||||
private static final Scanner READER = new Scanner(System.in, "UTF-8");
|
||||
private static AbstractConverter converter = null;
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
loadConverter();
|
||||
|
||||
List<String> input;
|
||||
do {
|
||||
OutputUtil.println("<Folder/File> [Recursions]:");
|
||||
input = readInput(2);
|
||||
} while (input.isEmpty());
|
||||
File folder = new File(input.get(0));
|
||||
|
||||
int recursionSteps = 1;
|
||||
if (input.size() > 1) {
|
||||
try {
|
||||
recursionSteps = Integer.parseInt(input.get(1));
|
||||
} catch (NumberFormatException e) {
|
||||
OutputUtil.println("Recursion steps is invalid and will be ignored.");
|
||||
}
|
||||
}
|
||||
|
||||
convertAllFiles(folder, recursionSteps);
|
||||
OutputUtil.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the user which converter they want, and assigns a converter instance to the converter variable
|
||||
*
|
||||
* @throws IOException <p>If there's a problem getting user input.</p>
|
||||
*/
|
||||
private static void loadConverter() throws IOException {
|
||||
int choice = getChoice("Which converter do you want do use?\n1. Anime to web mp4\n2. Audio converter\n" +
|
||||
"3. Video converter\n4. Web video converter", 1, 4);
|
||||
|
||||
OutputUtil.println("Input for this converter:");
|
||||
switch (choice) {
|
||||
case 1:
|
||||
animeConverter();
|
||||
break;
|
||||
case 2:
|
||||
converter = new AudioConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output extension>"));
|
||||
break;
|
||||
case 3:
|
||||
converter = new VideoConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output extension>"));
|
||||
break;
|
||||
case 4:
|
||||
converter = new WebVideoConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output extension>"));
|
||||
break;
|
||||
default:
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the file(s) as specified
|
||||
*
|
||||
* @param fileOrFolder <p>A file or a folder.</p>
|
||||
* @param recursionSteps <p>The depth to recurse if a folder is given.</p>
|
||||
* @throws IOException <p>If conversion or writing fails.</p>
|
||||
*/
|
||||
private static void convertAllFiles(File fileOrFolder, int recursionSteps) throws IOException {
|
||||
if (fileOrFolder.isDirectory()) {
|
||||
File[] files = FileUtil.listFilesRecursive(fileOrFolder, converter.getValidFormats(), recursionSteps);
|
||||
if (files != null && files.length > 0) {
|
||||
for (File file : files) {
|
||||
converter.convert(file);
|
||||
}
|
||||
} else {
|
||||
OutputUtil.println("No valid files found in folder.");
|
||||
}
|
||||
} else if (fileOrFolder.exists()) {
|
||||
converter.convert(fileOrFolder);
|
||||
} else {
|
||||
OutputUtil.println("Path " + fileOrFolder.getAbsolutePath() + " does not point to any file or folder.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the anime converter
|
||||
*
|
||||
* @throws IOException <p>If reading or writing fails.</p>
|
||||
*/
|
||||
private static void animeConverter() throws IOException {
|
||||
OutputUtil.println("[Audio languages jpn,eng,ger,fre] [Subtitle languages eng,ger,fre] [Convert to stereo if " +
|
||||
"necessary true/false] [Prevent signs&songs subtitles true/false]\nYour input: ");
|
||||
List<String> input = readInput(4);
|
||||
String[] audioLanguage = new String[]{"jpn", "0"};
|
||||
String[] subtitleLanguage = new String[]{"eng", "0"};
|
||||
boolean toStereo = true;
|
||||
boolean preventSigns = true;
|
||||
if (input.size() > 0 && ListUtil.getListFromCommaSeparatedString(input.get(0)) != null) {
|
||||
audioLanguage = ListUtil.getListFromCommaSeparatedString(input.get(0));
|
||||
}
|
||||
if (input.size() > 1 && ListUtil.getListFromCommaSeparatedString(input.get(1)) != null) {
|
||||
subtitleLanguage = ListUtil.getListFromCommaSeparatedString(input.get(1));
|
||||
}
|
||||
if (input.size() > 2) {
|
||||
toStereo = Boolean.parseBoolean(input.get(2));
|
||||
}
|
||||
if (input.size() > 3) {
|
||||
preventSigns = Boolean.parseBoolean(input.get(3));
|
||||
}
|
||||
converter = new AnimeConverter(FFPROBE_PATH, FFMPEG_PATH, audioLanguage, subtitleLanguage, toStereo, preventSigns);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reads a number of tokens from the user input
|
||||
*
|
||||
* @param max <p>The number of tokens expected.</p>
|
||||
* @return <p>A list of tokens.</p>
|
||||
*/
|
||||
private static List<String> readInput(int max) {
|
||||
List<String> tokens = tokenize(READER.nextLine());
|
||||
if (max < tokens.size()) {
|
||||
throw new IllegalArgumentException("Input contains " + tokens.size() +
|
||||
" arguments, but the input only supports " + max + " arguments.");
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user's choice
|
||||
*
|
||||
* @param prompt <p>The prompt shown to the user.</p>
|
||||
* @return <p>The non-empty choice given by the user.</p>
|
||||
* @throws IOException <p>If reading or writing fails.</p>
|
||||
*/
|
||||
private static String getChoice(String prompt) throws IOException {
|
||||
OutputUtil.println(prompt);
|
||||
String choice = "";
|
||||
while (choice.equals("")) {
|
||||
OutputUtil.println("Your input: ");
|
||||
choice = READER.nextLine();
|
||||
}
|
||||
return choice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an integer from the user
|
||||
*
|
||||
* @param prompt The prompt to give the user
|
||||
* @param min The minimum allowed value
|
||||
* @param max The maximum allowed value
|
||||
* @return The value given by the user
|
||||
*/
|
||||
private static int getChoice(String prompt, int min, int max) throws IOException {
|
||||
OutputUtil.println(prompt);
|
||||
int choice = Integer.MIN_VALUE;
|
||||
do {
|
||||
OutputUtil.println("Your input: ");
|
||||
try {
|
||||
choice = Integer.parseInt(READER.next());
|
||||
} catch (NumberFormatException e) {
|
||||
OutputUtil.println("Invalid choice. Please try again.");
|
||||
} finally {
|
||||
READER.nextLine();
|
||||
}
|
||||
} while (choice < min || choice > max);
|
||||
return choice;
|
||||
}
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
package net.knarcraft.ffmpegconverter.config;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.utility.FileHelper;
|
||||
import net.knarcraft.ffmpegconverter.utility.OutputUtil;
|
||||
import org.apache.commons.configuration2.Configuration;
|
||||
import org.apache.commons.configuration2.PropertiesConfiguration;
|
||||
import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder;
|
||||
import org.apache.commons.configuration2.builder.fluent.Configurations;
|
||||
import org.apache.commons.configuration2.ex.ConfigurationException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A handler for dealing with configurations
|
||||
*/
|
||||
public class ConfigHandler {
|
||||
|
||||
private final File configFolder = new File("conf").getAbsoluteFile();
|
||||
private final Configurations configurations = new Configurations();
|
||||
private final File settingsFile = new File(configFolder, "config.properties");
|
||||
private FileBasedConfigurationBuilder<PropertiesConfiguration> builder = configurations.propertiesBuilder(settingsFile);
|
||||
private final Map<ConfigKey<?>, Object> optionValues = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Gets the current value of a configuration option
|
||||
*
|
||||
* @param key <p>The configuration key to get the value of</p>
|
||||
* @return <p>The current value</p>
|
||||
*/
|
||||
@Nullable
|
||||
public Object getValue(@NotNull ConfigKey<?> key) {
|
||||
return optionValues.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a writable configuration used for changing settings
|
||||
*
|
||||
* @return <p>A writable properties configuration</p>
|
||||
*/
|
||||
@NotNull
|
||||
public PropertiesConfiguration getWritableConfiguration() {
|
||||
try {
|
||||
return builder.getConfiguration();
|
||||
} catch (ConfigurationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the writable configuration to disk
|
||||
*/
|
||||
public void writeConfiguration() {
|
||||
OutputUtil.printDebug("Preparing to save config");
|
||||
if (!configFolder.exists() && !configFolder.mkdir()) {
|
||||
throw new RuntimeException("Unable to create config folder. Make sure to run this .jar file from a " +
|
||||
"writable directory!");
|
||||
}
|
||||
try {
|
||||
if (!settingsFile.exists() && !settingsFile.createNewFile()) {
|
||||
OutputUtil.println("Failed to create configuration file.");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
OutputUtil.println("Failed to create configuration file.");
|
||||
}
|
||||
try {
|
||||
builder.save();
|
||||
} catch (ConfigurationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
OutputUtil.printDebug("Saved available hardware encoder handler");
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the saved configuration file
|
||||
*/
|
||||
public void load() throws IOException {
|
||||
optionValues.clear();
|
||||
Configuration configuration;
|
||||
if (!settingsFile.exists()) {
|
||||
configuration = new PropertiesConfiguration();
|
||||
BufferedReader reader = FileHelper.getBufferedReaderForInternalFile("/conf/config.properties");
|
||||
Map<String, String> entries = FileHelper.readKeyValuePairs(reader, "=");
|
||||
for (Map.Entry<String, String> entry : entries.entrySet()) {
|
||||
configuration.setProperty(entry.getKey(), entry.getValue());
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
configuration = configurations.properties(settingsFile);
|
||||
} catch (ConfigurationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
// Reload contents in the builder
|
||||
builder = configurations.propertiesBuilder(settingsFile);
|
||||
|
||||
for (@NotNull ConfigKey<?> key : ConfigKey.values()) {
|
||||
if (configuration.containsKey(key.toString())) {
|
||||
optionValues.put(key, configuration.get(key.getType(), key.toString()));
|
||||
} else {
|
||||
optionValues.put(key, key.getDefaultValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
package net.knarcraft.ffmpegconverter.config;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A representation of all configuration keys
|
||||
*/
|
||||
public class ConfigKey<T> {
|
||||
|
||||
private static final Set<ConfigKey<?>> allKeys = new HashSet<>();
|
||||
|
||||
/**
|
||||
* The configuration key for the list of hardware-accelerated encoders available on the system
|
||||
*/
|
||||
public static final ConfigKey<String> HARDWARE_ACCELERATED_ENCODERS = new ConfigKey<>("encoders-hardware-accelerated", String.class, "qsv,cuda,dxva2,d3d11va,opencl,vulkan");
|
||||
|
||||
/**
|
||||
* The configuration key for toggling debug mode
|
||||
*/
|
||||
public static final ConfigKey<Boolean> DEBUG = createKey("debug", Boolean.class, false);
|
||||
|
||||
/**
|
||||
* The configuration key for toggling hardware encoding
|
||||
*/
|
||||
public static final ConfigKey<Boolean> USE_HARDWARE_ENCODING = createKey("hardware-acceleration-encode", Boolean.class, false);
|
||||
|
||||
/**
|
||||
* The configuration key for toggling hardware decoding
|
||||
*/
|
||||
public static final ConfigKey<Boolean> USE_HARDWARE_DECODING = createKey("hardware-acceleration-decode", Boolean.class, true);
|
||||
|
||||
/**
|
||||
* The configuration key for toggling forced flac encoding
|
||||
*/
|
||||
public static final ConfigKey<Boolean> ENCODE_FLAC_ALWAYS = createKey("encode-flac-always", Boolean.class, false);
|
||||
|
||||
/**
|
||||
* The configuration key for anime audio languages
|
||||
*/
|
||||
public static final ConfigKey<String> AUDIO_LANGUAGES_ANIME = createKey("audio-languages-anime", String.class, "jpn,eng,*");
|
||||
|
||||
/**
|
||||
* The configuration key for anime subtitle languages
|
||||
*/
|
||||
public static final ConfigKey<String> SUBTITLE_LANGUAGES_ANIME = createKey("subtitle-languages-anime", String.class, "eng,*");
|
||||
|
||||
/**
|
||||
* The configuration key for the minimal subtitle preference
|
||||
*/
|
||||
public static final ConfigKey<String> MINIMAL_SUBTITLE_PREFERENCE = createKey("minimal-subtitle-preference", String.class, "AVOID");
|
||||
|
||||
/**
|
||||
* The configuration key for whether to de-interlace video streams
|
||||
*/
|
||||
public static final ConfigKey<Boolean> DE_INTERLACE_VIDEO = createKey("de-interlace-video", Boolean.class, false);
|
||||
|
||||
/**
|
||||
* The configuration key for whether to copy attached cover images
|
||||
*/
|
||||
public static final ConfigKey<Boolean> COPY_ATTACHED_IMAGES = createKey("copy-attached-images", Boolean.class, false);
|
||||
|
||||
/**
|
||||
* The configuration key for whether to overwrite the original file when converting
|
||||
*/
|
||||
public static final ConfigKey<Boolean> OVERWRITE_FILES = createKey("overwrite-files", Boolean.class, false);
|
||||
|
||||
private final String configKey;
|
||||
private final T defaultValue;
|
||||
private final Class<T> type;
|
||||
|
||||
/**
|
||||
* Instantiates a new config key
|
||||
*
|
||||
* @param configKey <p>The storage key in the configuration file</p>
|
||||
* @param type <p>The type of this option's value</p>
|
||||
* @param defaultValue <p>The default value of this option</p>
|
||||
*/
|
||||
private ConfigKey(@NotNull String configKey, @NotNull Class<T> type, @NotNull T defaultValue) {
|
||||
this.configKey = configKey;
|
||||
this.type = type;
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new config key
|
||||
*
|
||||
* @param configKey <p>The storage key in the configuration file</p>
|
||||
* @param type <p>The type of this option's value</p>
|
||||
* @param defaultValue <p>The default value of this option</p>
|
||||
* @param <F> <p>The type of key to create</p>
|
||||
* @return <p>The new config key</p>
|
||||
*/
|
||||
private static <F> ConfigKey<F> createKey(@NotNull String configKey, @NotNull Class<F> type, @NotNull F defaultValue) {
|
||||
ConfigKey<F> key = new ConfigKey<>(configKey, type, defaultValue);
|
||||
allKeys.add(key);
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the type of this option's value
|
||||
*
|
||||
* @return <p>The type of the value</p>
|
||||
*/
|
||||
@NotNull
|
||||
public Class<T> getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default value for the option represented by this key
|
||||
*
|
||||
* @return <p>The default value</p>
|
||||
*/
|
||||
@NotNull
|
||||
public T getDefaultValue() {
|
||||
return this.defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.configKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all configuration keys
|
||||
*
|
||||
* @return <p>All configuration keys</p>
|
||||
*/
|
||||
@NotNull
|
||||
public static ConfigKey<?>[] values() {
|
||||
return allKeys.toArray(new ConfigKey[0]);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,185 @@
|
||||
package net.knarcraft.ffmpegconverter.config;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.property.MinimalSubtitlePreference;
|
||||
import net.knarcraft.ffmpegconverter.utility.ConfigHelper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A configuration used for FFMpegConvert
|
||||
*/
|
||||
public class Configuration {
|
||||
|
||||
private final ConfigHandler configHandler;
|
||||
private boolean debug;
|
||||
private boolean useHardwareEncoding;
|
||||
private boolean useHardwareDecoding;
|
||||
private List<String> hardwareEncoders;
|
||||
private boolean alwaysEncodeFlac;
|
||||
private List<String> animeAudioLanguages;
|
||||
private List<String> animeSubtitleLanguages;
|
||||
private MinimalSubtitlePreference minimalSubtitlePreference;
|
||||
private boolean deInterlaceVideo;
|
||||
private boolean copyAttachedImages;
|
||||
private boolean overwriteFiles;
|
||||
|
||||
/**
|
||||
* Instantiates and loads a new configuration
|
||||
*/
|
||||
public Configuration() {
|
||||
this.configHandler = new ConfigHandler();
|
||||
try {
|
||||
this.configHandler.load();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
load();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads/reloads this configuration
|
||||
*/
|
||||
public void load() {
|
||||
try {
|
||||
this.configHandler.load();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
debug = ConfigHelper.asBoolean(configHandler.getValue(ConfigKey.DEBUG));
|
||||
useHardwareEncoding = ConfigHelper.asBoolean(configHandler.getValue(ConfigKey.USE_HARDWARE_ENCODING));
|
||||
useHardwareDecoding = ConfigHelper.asBoolean(configHandler.getValue(ConfigKey.USE_HARDWARE_DECODING));
|
||||
hardwareEncoders = ConfigHelper.asStringList(configHandler.getValue(ConfigKey.HARDWARE_ACCELERATED_ENCODERS));
|
||||
alwaysEncodeFlac = ConfigHelper.asBoolean(configHandler.getValue(ConfigKey.ENCODE_FLAC_ALWAYS));
|
||||
animeAudioLanguages = ConfigHelper.asStringList(configHandler.getValue(ConfigKey.AUDIO_LANGUAGES_ANIME));
|
||||
animeSubtitleLanguages = ConfigHelper.asStringList(configHandler.getValue(ConfigKey.SUBTITLE_LANGUAGES_ANIME));
|
||||
deInterlaceVideo = ConfigHelper.asBoolean(configHandler.getValue(ConfigKey.DE_INTERLACE_VIDEO));
|
||||
copyAttachedImages = ConfigHelper.asBoolean(configHandler.getValue(ConfigKey.COPY_ATTACHED_IMAGES));
|
||||
overwriteFiles = ConfigHelper.asBoolean(configHandler.getValue(ConfigKey.OVERWRITE_FILES));
|
||||
try {
|
||||
minimalSubtitlePreference = MinimalSubtitlePreference.valueOf(String.valueOf(configHandler.getValue(
|
||||
ConfigKey.MINIMAL_SUBTITLE_PREFERENCE)));
|
||||
} catch (IllegalArgumentException | NullPointerException exception) {
|
||||
minimalSubtitlePreference = MinimalSubtitlePreference.AVOID;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the underlying config handler for this configuration
|
||||
*
|
||||
* @return <p>The underlying config handler</p>
|
||||
*/
|
||||
@NotNull
|
||||
public ConfigHandler getConfigHandler() {
|
||||
return this.configHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether debug mode is enabled
|
||||
*
|
||||
* @return <p>True if debug mode is enabled</p>
|
||||
*/
|
||||
public boolean isDebugEnabled() {
|
||||
return this.debug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether hardware accelerated encoding is enabled
|
||||
*
|
||||
* @return <p>True if hardware accelerated encoding is enabled</p>
|
||||
*/
|
||||
public boolean useHardwareEncoding() {
|
||||
return this.useHardwareEncoding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether hardware accelerated decoding is enabled
|
||||
*
|
||||
* @return <p>True if hardware accelerated decoding is enabled</p>
|
||||
*/
|
||||
public boolean useHardwareDecoding() {
|
||||
return this.useHardwareDecoding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether FLAC audio tracks should always be re-encoded
|
||||
*
|
||||
* @return <p>Whether FLAC tracks should always be re-encoded</p>
|
||||
*/
|
||||
public boolean alwaysEncodeFlac() {
|
||||
return this.alwaysEncodeFlac;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all hardware encoders defined in the configuration
|
||||
*
|
||||
* @return <p>The specified hardware encoders</p>
|
||||
*/
|
||||
@NotNull
|
||||
public List<String> hardwareEncoders() {
|
||||
return this.hardwareEncoders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the audio language priorities for usage when converting anime
|
||||
*
|
||||
* @return <p>The anime audio language priorities</p>
|
||||
*/
|
||||
@NotNull
|
||||
public List<String> getAnimeAudioLanguages() {
|
||||
return this.animeAudioLanguages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the subtitle language priorities for usage when converting anime
|
||||
*
|
||||
* @return <p>The anime subtitle language priorities</p>
|
||||
*/
|
||||
@NotNull
|
||||
public List<String> getAnimeSubtitleLanguages() {
|
||||
return this.animeSubtitleLanguages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the preference for minimal subtitles
|
||||
*
|
||||
* @return <p>The minimal subtitle preference</p>
|
||||
*/
|
||||
@NotNull
|
||||
public MinimalSubtitlePreference getMinimalSubtitlePreference() {
|
||||
return this.minimalSubtitlePreference;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether video streams should be de-interlaced
|
||||
*
|
||||
* @return <p>Whether to de-interlace streams</p>
|
||||
*/
|
||||
public boolean deInterlaceVideo() {
|
||||
return this.deInterlaceVideo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether attached images should be copied
|
||||
*
|
||||
* <p>FFMpeg sometimes throws errors when including attached images.</p>
|
||||
*
|
||||
* @return <p>True if attached images should be copied</p>
|
||||
*/
|
||||
public boolean copyAttachedImages() {
|
||||
return this.copyAttachedImages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether the original files should be overwritten after conversion has been finished
|
||||
*
|
||||
* @return <p>True if the original file should be overwritten</p>
|
||||
*/
|
||||
public boolean overwriteFiles() {
|
||||
return this.overwriteFiles;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,155 @@
|
||||
package net.knarcraft.ffmpegconverter.container;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A class for generating and storing a ffmpeg command
|
||||
*/
|
||||
public class FFMpegCommand implements Cloneable {
|
||||
|
||||
private @NotNull String executable;
|
||||
private @NotNull List<String> globalOptions;
|
||||
private @NotNull List<String> inputFileOptions;
|
||||
private @NotNull List<String> inputFiles;
|
||||
private @NotNull List<String> outputFileOptions;
|
||||
private @Nullable String outputVideoCodec;
|
||||
private @NotNull String outputFile;
|
||||
|
||||
/**
|
||||
* Instantiates a new FFMPEG command
|
||||
*
|
||||
* @param executable <p>The FFMPEG/FFPROBE executable to run</p>
|
||||
*/
|
||||
public FFMpegCommand(@NotNull String executable) {
|
||||
this.executable = executable;
|
||||
this.globalOptions = new ArrayList<>();
|
||||
this.inputFileOptions = new ArrayList<>();
|
||||
this.inputFiles = new ArrayList<>();
|
||||
this.outputFileOptions = new ArrayList<>();
|
||||
this.outputFile = "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a global ffmpeg option to this command
|
||||
*
|
||||
* @param argument <p>The option(s) to add</p>
|
||||
*/
|
||||
public void addGlobalOption(@NotNull String... argument) {
|
||||
this.globalOptions.addAll(List.of(argument));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an input file option to this command
|
||||
*
|
||||
* @param argument <p>The input file option(s) to add</p>
|
||||
*/
|
||||
public void addInputFileOption(@NotNull String... argument) {
|
||||
this.inputFileOptions.addAll(List.of(argument));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an input file to this command
|
||||
*
|
||||
* <p>Note that this adds the "-i", so don't add that yourself!</p>
|
||||
*
|
||||
* @param argument <p>The input file(s) to add</p>
|
||||
*/
|
||||
public void addInputFile(@NotNull String... argument) {
|
||||
for (String fileName : argument) {
|
||||
if (fileName.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
this.inputFiles.add(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the input files currently added to this command
|
||||
*
|
||||
* @return <p>The input files</p>
|
||||
*/
|
||||
@NotNull
|
||||
public List<String> getInputFiles() {
|
||||
return new ArrayList<>(this.inputFiles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an output file option to this command
|
||||
*
|
||||
* @param argument <p>The output file option(s) to add</p>
|
||||
*/
|
||||
public void addOutputFileOption(@NotNull String... argument) {
|
||||
this.outputFileOptions.addAll(List.of(argument));
|
||||
|
||||
// Detect when the output video codec is set
|
||||
for (int i = 0; i < argument.length; i++) {
|
||||
String item = argument[i].trim().toLowerCase();
|
||||
if (i < argument.length - 1 && item.equals("-vcodec") || item.equals("-codec:v") || item.equals("-c:v")) {
|
||||
this.outputVideoCodec = argument[i + 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the output file for this command
|
||||
*
|
||||
* @param argument <p>The path to the output file</p>
|
||||
*/
|
||||
public void setOutputFile(@NotNull String argument) {
|
||||
this.outputFile = argument;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the output video codec set in this command
|
||||
*
|
||||
* @return <p>The output video codec, or null if not set</p>
|
||||
*/
|
||||
@Nullable
|
||||
public String getOutputVideoCodec() {
|
||||
return this.outputVideoCodec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the result of combining all the given input
|
||||
*
|
||||
* @return <p>The generated FFMPEG command</p>
|
||||
*/
|
||||
@NotNull
|
||||
public String[] getResult() {
|
||||
List<String> result = new ArrayList<>();
|
||||
result.add(executable);
|
||||
result.addAll(globalOptions);
|
||||
result.addAll(inputFileOptions);
|
||||
for (String inputFile : inputFiles) {
|
||||
result.add("-i");
|
||||
result.add(inputFile);
|
||||
}
|
||||
result.addAll(outputFileOptions);
|
||||
if (!outputFile.isEmpty()) {
|
||||
result.add(outputFile);
|
||||
}
|
||||
return result.toArray(new String[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FFMpegCommand clone() {
|
||||
try {
|
||||
FFMpegCommand clone = (FFMpegCommand) super.clone();
|
||||
clone.outputVideoCodec = this.outputVideoCodec;
|
||||
clone.outputFile = this.outputFile;
|
||||
clone.executable = this.executable;
|
||||
clone.globalOptions = new ArrayList<>(this.globalOptions);
|
||||
clone.inputFileOptions = new ArrayList<>(this.inputFileOptions);
|
||||
clone.inputFiles = new ArrayList<>(this.inputFiles);
|
||||
clone.outputFileOptions = new ArrayList<>(this.outputFileOptions);
|
||||
return clone;
|
||||
} catch (CloneNotSupportedException e) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package net.knarcraft.ffmpegconverter.container;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* The result of running a process
|
||||
*
|
||||
* @param exitCode <p>The exit code the process exited with</p>
|
||||
* @param output <p>The output the process produced</p>
|
||||
*/
|
||||
public record ProcessResult(int exitCode, @NotNull String output) {
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
package net.knarcraft.ffmpegconverter.container;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.streams.AudioStream;
|
||||
import net.knarcraft.ffmpegconverter.streams.OtherStream;
|
||||
import net.knarcraft.ffmpegconverter.streams.StreamObject;
|
||||
import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
|
||||
import net.knarcraft.ffmpegconverter.streams.VideoStream;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A record for storing the result of probing for streams
|
||||
*
|
||||
* @param parsedFiles <p>The files that were parsed to get the attached streams</p>
|
||||
* @param parsedStreams <p>The streams that were parsed from the files</p>
|
||||
*/
|
||||
public record StreamProbeResult(@NotNull List<File> parsedFiles, @NotNull List<StreamObject> parsedStreams) {
|
||||
|
||||
/**
|
||||
* Gets all probed subtitle streams
|
||||
*
|
||||
* @return <p>All probed subtitle streams</p>
|
||||
*/
|
||||
@NotNull
|
||||
public List<SubtitleStream> getSubtitleStreams() {
|
||||
return filterStreamsByType(this.parsedStreams, SubtitleStream.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all probed audio streams
|
||||
*
|
||||
* @return <p>All probed audio streams</p>
|
||||
*/
|
||||
@NotNull
|
||||
public List<AudioStream> getAudioStreams() {
|
||||
return filterStreamsByType(this.parsedStreams, AudioStream.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all probed video streams
|
||||
*
|
||||
* @return <p>All probed video streams</p>
|
||||
*/
|
||||
@NotNull
|
||||
public List<VideoStream> getVideoStreams() {
|
||||
return filterStreamsByType(this.parsedStreams, VideoStream.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all other streams
|
||||
*
|
||||
* <p>Other streams are streams that are not video, audio or subtitle streams</p>
|
||||
*
|
||||
* @return <p>All other streams</p>
|
||||
*/
|
||||
@NotNull
|
||||
public List<OtherStream> getOtherStreams() {
|
||||
return filterStreamsByType(this.parsedStreams, OtherStream.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters parsed streams into one of the stream types
|
||||
*
|
||||
* @param streams <p>A list of stream objects.</p>
|
||||
* @param clazz <p>The class to filter</p>
|
||||
* @param <G> <p>The correct object type for the streams with the selected codec type.</p>
|
||||
* @return <p>A potentially shorter list of streams.</p>
|
||||
*/
|
||||
@NotNull
|
||||
@SuppressWarnings("unchecked")
|
||||
private <G extends StreamObject> List<G> filterStreamsByType(@NotNull List<StreamObject> streams,
|
||||
@NotNull Class<?> clazz) {
|
||||
List<G> newStreams = new ArrayList<>();
|
||||
for (StreamObject stream : streams) {
|
||||
if (stream.getClass() == clazz) {
|
||||
newStreams.add((G) stream);
|
||||
}
|
||||
}
|
||||
return newStreams;
|
||||
}
|
||||
|
||||
}
|
@ -1,12 +1,18 @@
|
||||
package net.knarcraft.ffmpegconverter.converter;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.streams.AudioStream;
|
||||
import net.knarcraft.ffmpegconverter.FFMpegConvert;
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.DeInterlaceModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ModuleExecutor;
|
||||
import net.knarcraft.ffmpegconverter.handler.AvailableHardwareEncoderHandler;
|
||||
import net.knarcraft.ffmpegconverter.streams.StreamObject;
|
||||
import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
|
||||
import net.knarcraft.ffmpegconverter.streams.VideoStream;
|
||||
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
||||
import net.knarcraft.ffmpegconverter.utility.FileHelper;
|
||||
import net.knarcraft.ffmpegconverter.utility.FileUtil;
|
||||
import net.knarcraft.ffmpegconverter.utility.OutputUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@ -17,174 +23,149 @@ import java.util.List;
|
||||
* Implements all methods which can be useful for any implementation of a converter.
|
||||
*/
|
||||
public abstract class AbstractConverter implements Converter {
|
||||
final boolean debug = false;
|
||||
|
||||
final boolean debug = FFMpegConvert.getConfiguration().isDebugEnabled();
|
||||
private final String newExtension;
|
||||
String ffprobePath;
|
||||
String ffmpegPath;
|
||||
String[] audioFormats;
|
||||
String[] videoFormats;
|
||||
protected String ffprobePath;
|
||||
protected String ffmpegPath;
|
||||
protected List<String> audioFormats;
|
||||
protected List<String> videoFormats;
|
||||
protected List<String> subtitleFormats;
|
||||
protected AvailableHardwareEncoderHandler encoderHandler = null;
|
||||
|
||||
|
||||
/**
|
||||
* Initializes variables used by the abstract converter
|
||||
*/
|
||||
AbstractConverter(String newExtension) {
|
||||
AbstractConverter(@Nullable String newExtension) {
|
||||
this.newExtension = newExtension;
|
||||
OutputUtil.setDebug(this.debug);
|
||||
try {
|
||||
audioFormats = FileUtil.readFileLines("audio_formats.txt");
|
||||
videoFormats = FileUtil.readFileLines("video_formats.txt");
|
||||
this.audioFormats = FileHelper.readLines(FileHelper.getBufferedReaderForInternalFile("/audio_formats.txt"));
|
||||
this.videoFormats = FileHelper.readLines(FileHelper.getBufferedReaderForInternalFile("/video_formats.txt"));
|
||||
this.subtitleFormats = FileHelper.readLines(FileHelper.getBufferedReaderForInternalFile("/subtitle_formats.txt"));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
OutputUtil.println("Unable to read audio and/or video formats from internal files.");
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters parsed streams into one of the stream types
|
||||
*
|
||||
* @param streams <p>A list of stream objects.</p>
|
||||
* @param clazz <p>The class to filter</p>
|
||||
* @param <G> <p>The correct object type for the streams with the selected codec type.</p>
|
||||
* @return <p>A potentially shorter list of streams.</p>
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
static <G extends StreamObject> List<G> filterStreamsByType(List<StreamObject> streams, Class<?> clazz) {
|
||||
List<G> newStreams = new ArrayList<>();
|
||||
for (StreamObject stream : streams) {
|
||||
if (stream.getClass() == clazz) {
|
||||
newStreams.add((G) stream);
|
||||
}
|
||||
}
|
||||
return newStreams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters and sorts audio streams according to chosen languages
|
||||
*
|
||||
* @param audioStreams <p>A list of audio streams.</p>
|
||||
* @param audioLanguages <p>A list of languages.</p>
|
||||
* @return <p>A list containing just audio tracks of chosen languages, sorted in order of languages.</p>
|
||||
*/
|
||||
static List<AudioStream> filterAudioStreams(List<AudioStream> audioStreams, String[] audioLanguages) {
|
||||
return sortStreamsByLanguage(audioStreams, audioLanguages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters and sorts subtitle streams according to chosen languages
|
||||
*
|
||||
* @param subtitleStreams <p>A list of subtitle streams.</p>
|
||||
* @param subtitleLanguages <p>A list of languages.</p>
|
||||
* @param preventSignsAndSongs <p>Whether partial subtitles should be avoided.</p>
|
||||
* @return <p>A list containing just subtitles of chosen languages, sorted in order of languages.</p>
|
||||
*/
|
||||
static List<SubtitleStream> filterSubtitleStreams(List<SubtitleStream> subtitleStreams, String[] subtitleLanguages,
|
||||
boolean preventSignsAndSongs) {
|
||||
List<SubtitleStream> sorted = sortStreamsByLanguage(subtitleStreams, subtitleLanguages);
|
||||
sorted.removeIf((stream) -> preventSignsAndSongs && !stream.getIsFullSubtitle());
|
||||
return sorted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts subtitle streams according to chosen languages and removes non matching languages
|
||||
*
|
||||
* @param streams <p>A list of streams to sort.</p>
|
||||
* @param languages <p>The languages chosen by the user.</p>
|
||||
* @param <G> <p>The type of streams to sort.</p>
|
||||
* @return <p>A sorted version of the list.</p>
|
||||
*/
|
||||
private static <G extends StreamObject> List<G> sortStreamsByLanguage(List<G> streams, String[] languages) {
|
||||
List<G> sorted = new ArrayList<>();
|
||||
for (String language : languages) {
|
||||
for (G stream : streams) {
|
||||
String streamLanguage = stream.getLanguage();
|
||||
if (language.equals("*") || ((streamLanguage == null || streamLanguage.equals("und")) &&
|
||||
language.equals("0")) || (streamLanguage != null && streamLanguage.equals(language))) {
|
||||
sorted.add(stream);
|
||||
}
|
||||
}
|
||||
streams.removeAll(sorted);
|
||||
}
|
||||
return sorted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all valid input formats for the converter
|
||||
*
|
||||
* @return <p>A list of valid input formats</p>
|
||||
*/
|
||||
public abstract String[] getValidFormats();
|
||||
|
||||
/**
|
||||
* Reads streams from a file, and converts it to an mp4
|
||||
*
|
||||
* @param folder <p>The folder of the file to process.</p>
|
||||
* @param file <p>The file to process.</p>
|
||||
* @throws IOException <p>If the BufferedReader fails.</p>
|
||||
*/
|
||||
private void processFile(File folder, File file) throws IOException {
|
||||
List<StreamObject> streams = FFMpegHelper.probeFile(ffprobePath, file);
|
||||
if (streams.isEmpty()) {
|
||||
@Override
|
||||
public void convert(@NotNull File file) throws IOException {
|
||||
StreamProbeResult probeResult = FFMpegHelper.probeFile(this.ffprobePath, file, this.subtitleFormats);
|
||||
if (probeResult.parsedStreams().isEmpty()) {
|
||||
throw new IllegalArgumentException("The file has no valid streams. Please make sure the file exists and" +
|
||||
" is not corrupt.");
|
||||
}
|
||||
String newPath = FileUtil.getNonCollidingPath(folder, file, newExtension);
|
||||
|
||||
String outExtension = this.newExtension != null ? this.newExtension : FileUtil.getExtension(file.getName());
|
||||
String newPath = FileUtil.getNonCollidingPath(file.getParentFile(), file, outExtension);
|
||||
OutputUtil.println();
|
||||
OutputUtil.println("Preparing to start process...");
|
||||
OutputUtil.println("Converting " + file);
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(generateConversionCommand(ffmpegPath, file, streams, newPath));
|
||||
FFMpegHelper.runProcess(processBuilder, folder, "\n", true);
|
||||
|
||||
FFMpegCommand ffMpegCommand = generateConversionCommand(this.ffmpegPath, probeResult, newPath);
|
||||
// If the command is null, that means the file does not need conversion
|
||||
if (ffMpegCommand == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// De-interlace the video if enabled
|
||||
if (FFMpegConvert.getConfiguration().deInterlaceVideo() &&
|
||||
(outExtension.equalsIgnoreCase("mkv") || outExtension.equalsIgnoreCase("mp4"))) {
|
||||
new ModuleExecutor(ffMpegCommand, List.of(new DeInterlaceModule())).execute();
|
||||
}
|
||||
|
||||
String[] command = ffMpegCommand.getResult();
|
||||
// If no commands were given, no conversion is necessary
|
||||
if (command.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(command);
|
||||
int exitCode = FFMpegHelper.runProcess(processBuilder, file.getParentFile(), "\n", true).exitCode();
|
||||
if (exitCode != 0) {
|
||||
handleError(ffMpegCommand, file, newPath);
|
||||
} else if (FFMpegConvert.getConfiguration().overwriteFiles() &&
|
||||
FileUtil.getExtension(newPath).equalsIgnoreCase(FileUtil.getExtension(file.getPath()))) {
|
||||
File outputFile = new File(newPath);
|
||||
if (!file.delete()) {
|
||||
OutputUtil.println("Unable to remove original file.");
|
||||
System.exit(1);
|
||||
} else if (!outputFile.renameTo(file)) {
|
||||
OutputUtil.println("Failed to re-name file after conversion!");
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first audio stream from a list of streams
|
||||
* Handles an ffmpeg conversion error
|
||||
*
|
||||
* @param streams <p>A list of all streams.</p>
|
||||
* @return <p>The first audio stream found or null if no audio streams were found.</p>
|
||||
* @param ffMpegCommand <p>The failed ffmpeg command</p>
|
||||
* @param file <p>The file that was to be converted</p>
|
||||
* @param newPath <p>The path of the output file</p>
|
||||
* @throws IOException <p>If unable to produce output</p>
|
||||
*/
|
||||
AudioStream getFirstAudioSteam(List<StreamObject> streams) {
|
||||
List<AudioStream> audioStreams = filterStreamsByType(streams, AudioStream.class);
|
||||
AudioStream audioStream = null;
|
||||
if (audioStreams.size() > 0) {
|
||||
audioStream = audioStreams.get(0);
|
||||
private void handleError(@NotNull FFMpegCommand ffMpegCommand, @NotNull File file,
|
||||
@NotNull String newPath) throws IOException {
|
||||
File outputFile = new File(newPath);
|
||||
if (outputFile.exists() && !outputFile.delete()) {
|
||||
OutputUtil.println("Failed to remove failed output file. Please remove it manually");
|
||||
}
|
||||
return audioStream;
|
||||
|
||||
String outputCodec = ffMpegCommand.getOutputVideoCodec();
|
||||
if (outputCodec != null && outputCodec.contains("_")) {
|
||||
String[] parts = outputCodec.split("_");
|
||||
String hardwareAcceleration = parts[parts.length - 1];
|
||||
List<String> encodingMethods = getAvailableHardwareEncodingMethods();
|
||||
if (encodingMethods.contains(hardwareAcceleration)) {
|
||||
OutputUtil.println("Disabling " + hardwareAcceleration + " hardware acceleration");
|
||||
encoderHandler.removeHardwareEncoding(hardwareAcceleration);
|
||||
encoderHandler.save();
|
||||
convert(file);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Conversion failed. Please check the preceding output.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first subtitle stream from a list of streams
|
||||
* Gets the available methods for hardware encoding
|
||||
*
|
||||
* @param streams <p>A list of all streams.</p>
|
||||
* @return <p>The first subtitle stream found or null if no subtitle streams were found.</p>
|
||||
* @return <p>Available hardware encoding methods</p>
|
||||
*/
|
||||
SubtitleStream getFirstSubtitleStream(List<StreamObject> streams) {
|
||||
List<SubtitleStream> subtitleStreams = filterStreamsByType(streams, SubtitleStream.class);
|
||||
SubtitleStream subtitleStream = null;
|
||||
if (subtitleStreams.size() > 0) {
|
||||
subtitleStream = subtitleStreams.get(0);
|
||||
@NotNull
|
||||
protected List<String> getAvailableHardwareEncodingMethods() {
|
||||
if (encoderHandler == null) {
|
||||
encoderHandler = AvailableHardwareEncoderHandler.load();
|
||||
if (encoderHandler.availableHardwareEncodings().isEmpty()) {
|
||||
List<String> hardwareEncoding;
|
||||
try {
|
||||
hardwareEncoding = new ArrayList<>(FFMpegHelper.getHWAcceleration(ffmpegPath));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return subtitleStream;
|
||||
hardwareEncoding.remove(0);
|
||||
encoderHandler = new AvailableHardwareEncoderHandler(hardwareEncoding);
|
||||
encoderHandler.save();
|
||||
}
|
||||
}
|
||||
|
||||
return encoderHandler.availableHardwareEncodings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first video stream from a list of streams
|
||||
* Sets the output indexes for the given streams
|
||||
*
|
||||
* @param streams <p>A list of all streams.</p>
|
||||
* @return <p>The first video stream found or null if no video streams were found.</p>
|
||||
* <p>This will basically mark the given streams' order as the order they will appear in the output file. This is
|
||||
* used when specifying specific codecs per-stream in the output file.</p>
|
||||
*
|
||||
* @param streams <p>The streams to set the output indexes for</p>
|
||||
*/
|
||||
VideoStream getFirstVideoStream(List<StreamObject> streams) {
|
||||
List<VideoStream> videoStreams = filterStreamsByType(streams, VideoStream.class);
|
||||
VideoStream videoStream = null;
|
||||
if (videoStreams.size() > 0) {
|
||||
videoStream = videoStreams.get(0);
|
||||
protected <K extends StreamObject> void setOutputIndexes(@NotNull List<K> streams) {
|
||||
for (int i = 0; i < streams.size(); i++) {
|
||||
streams.get(i).setOutputIndex(i);
|
||||
}
|
||||
if (videoStream == null) {
|
||||
throw new IllegalArgumentException("The file does not have any valid video streams.");
|
||||
}
|
||||
return videoStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void convert(File file) throws IOException {
|
||||
processFile(file.getParentFile(), file);
|
||||
}
|
||||
}
|
||||
|
@ -1,74 +1,185 @@
|
||||
package net.knarcraft.ffmpegconverter.converter;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.FFMpegConvert;
|
||||
import net.knarcraft.ffmpegconverter.config.Configuration;
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.DebugModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ModuleExecutor;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.hardwarecoding.H265HardwareEncodingModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.hardwarecoding.HardwareDecodeModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.mapping.MapAllModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.mapping.SetDefaultStreamModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.CopyAudioModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.CopySubtitlesModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.CopyVideoModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.FastStartModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.SetQualityModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.SetStreamLanguageModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.SetVideoCodecModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.sorter.AudioLanguageSorter;
|
||||
import net.knarcraft.ffmpegconverter.converter.sorter.ForcedFirstSorter;
|
||||
import net.knarcraft.ffmpegconverter.converter.sorter.MinimalSubtitleSorter;
|
||||
import net.knarcraft.ffmpegconverter.converter.sorter.SpecialAudioSorter;
|
||||
import net.knarcraft.ffmpegconverter.converter.sorter.StreamSorter;
|
||||
import net.knarcraft.ffmpegconverter.converter.sorter.SubtitleLanguageSorter;
|
||||
import net.knarcraft.ffmpegconverter.converter.sorter.SubtitleTitleSorter;
|
||||
import net.knarcraft.ffmpegconverter.property.MinimalSubtitlePreference;
|
||||
import net.knarcraft.ffmpegconverter.streams.AudioStream;
|
||||
import net.knarcraft.ffmpegconverter.streams.StreamObject;
|
||||
import net.knarcraft.ffmpegconverter.streams.OtherStream;
|
||||
import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
|
||||
import net.knarcraft.ffmpegconverter.streams.VideoStream;
|
||||
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
||||
import net.knarcraft.ffmpegconverter.utility.OutputUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A converter mainly designed for converting anime to web-playable mp4
|
||||
* A converter for converting anime, keeping all streams
|
||||
*/
|
||||
public class AnimeConverter extends AbstractConverter {
|
||||
private final String[] audioLanguages;
|
||||
private final String[] subtitleLanguages;
|
||||
private final boolean toStereo;
|
||||
private final boolean preventSignsAndSongs;
|
||||
|
||||
private final List<String> audioLanguages;
|
||||
private final List<String> subtitleLanguages;
|
||||
private final MinimalSubtitlePreference subtitlePreference;
|
||||
private final int forcedAudioIndex;
|
||||
private final int forcedSubtitleIndex;
|
||||
private final String subtitleNameFilter;
|
||||
private final boolean forceVideoEncoding;
|
||||
private final boolean forceAudioEncoding;
|
||||
|
||||
/**
|
||||
* Instantiates a new anime converter
|
||||
*
|
||||
* @param ffprobePath <p>Path/command to ffprobe.</p>
|
||||
* @param ffmpegPath <p>Path/command to ffmpeg.</p>
|
||||
* @param audioLanguages <p>List of wanted audio languages in descending order.</p>
|
||||
* @param subtitleLanguages <p>List of wanted subtitle languages in descending order.</p>
|
||||
* @param toStereo <p>Convert video with several audio channels to stereo.</p>
|
||||
* @param preventSignsAndSongs <p>Prevent subtitles only converting signs and songs (not speech).</p>
|
||||
* @param forcedAudioIndex <p>A specific audio stream to force as default. 0-indexed from the first audio stream found</p>
|
||||
* @param forcedSubtitleIndex <p>A specific subtitle stream to force as default. 0-indexed for the first subtitle stream found</p>
|
||||
* @param forceVideoEncoding <p>Whether to enforce encoding on the video, even if already hevc</p>
|
||||
* @param forceAudioEncoding <p>Whether to convert audio to web-playable, even though there should be no need</p>
|
||||
*/
|
||||
public AnimeConverter(String ffprobePath, String ffmpegPath, String[] audioLanguages, String[] subtitleLanguages,
|
||||
boolean toStereo, boolean preventSignsAndSongs) {
|
||||
super("mp4");
|
||||
public AnimeConverter(@NotNull String ffprobePath, @NotNull String ffmpegPath, int forcedAudioIndex,
|
||||
int forcedSubtitleIndex, @NotNull String subtitleNameFilter, boolean forceVideoEncoding,
|
||||
boolean forceAudioEncoding) {
|
||||
super("mkv");
|
||||
Configuration configuration = FFMpegConvert.getConfiguration();
|
||||
this.ffprobePath = ffprobePath;
|
||||
this.ffmpegPath = ffmpegPath;
|
||||
this.audioLanguages = audioLanguages;
|
||||
this.subtitleLanguages = subtitleLanguages;
|
||||
this.toStereo = toStereo;
|
||||
this.preventSignsAndSongs = preventSignsAndSongs;
|
||||
this.audioLanguages = configuration.getAnimeAudioLanguages();
|
||||
this.subtitleLanguages = configuration.getAnimeSubtitleLanguages();
|
||||
this.subtitlePreference = configuration.getMinimalSubtitlePreference();
|
||||
this.forcedAudioIndex = forcedAudioIndex;
|
||||
this.forcedSubtitleIndex = forcedSubtitleIndex;
|
||||
this.subtitleNameFilter = subtitleNameFilter;
|
||||
this.forceVideoEncoding = forceVideoEncoding;
|
||||
this.forceAudioEncoding = forceAudioEncoding;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] generateConversionCommand(String executable, File file, List<StreamObject> streams, String outFile) {
|
||||
List<String> command = FFMpegHelper.getFFMpegWebVideoCommand(executable, file.getName());
|
||||
@Nullable
|
||||
public FFMpegCommand generateConversionCommand(@NotNull String executable,
|
||||
@NotNull StreamProbeResult probeResult,
|
||||
@NotNull String outFile) {
|
||||
Configuration configuration = FFMpegConvert.getConfiguration();
|
||||
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
|
||||
List<ConverterModule> modules = new ArrayList<>();
|
||||
if (this.debug) {
|
||||
FFMpegHelper.addDebugArguments(command, 50, 120);
|
||||
modules.add(new DebugModule());
|
||||
}
|
||||
modules.add(new FastStartModule());
|
||||
|
||||
//Map all video streams
|
||||
List<VideoStream> videoStreams = probeResult.getVideoStreams();
|
||||
modules.add(new MapAllModule<>(videoStreams));
|
||||
setOutputIndexes(videoStreams);
|
||||
|
||||
//Get the first audio stream in accordance with chosen languages
|
||||
List<AudioStream> audioStreams = filterAudioStreams(filterStreamsByType(streams, AudioStream.class), audioLanguages);
|
||||
AudioStream audioStream = getFirstAudioSteam(new ArrayList<>(audioStreams));
|
||||
StreamSorter<AudioStream> audioSorter = new AudioLanguageSorter(this.audioLanguages)
|
||||
.append(new ForcedFirstSorter<>(this.forcedAudioIndex))
|
||||
.append(new SpecialAudioSorter(MinimalSubtitlePreference.REJECT));
|
||||
List<AudioStream> sortedAudio = audioSorter.chainSort(probeResult.getAudioStreams());
|
||||
setOutputIndexes(sortedAudio);
|
||||
modules.add(new MapAllModule<>(sortedAudio));
|
||||
modules.add(new SetDefaultStreamModule<>(sortedAudio, 0));
|
||||
modules.add(new SetStreamLanguageModule<>(sortedAudio));
|
||||
|
||||
//Get the first subtitle stream in accordance with chosen languages and signs and songs prevention
|
||||
List<SubtitleStream> subtitleStreams = filterSubtitleStreams(filterStreamsByType(streams,
|
||||
SubtitleStream.class), subtitleLanguages, preventSignsAndSongs);
|
||||
SubtitleStream subtitleStream = getFirstSubtitleStream(new ArrayList<>(subtitleStreams));
|
||||
StreamSorter<SubtitleStream> subtitleSorter = new SubtitleTitleSorter(
|
||||
List.of(this.subtitleNameFilter.split(",")))
|
||||
.append(new SubtitleLanguageSorter(this.subtitleLanguages))
|
||||
.append(new MinimalSubtitleSorter(this.subtitlePreference))
|
||||
.append(new ForcedFirstSorter<>(this.forcedSubtitleIndex));
|
||||
List<SubtitleStream> sortedSubtitles = subtitleSorter.chainSort(probeResult.getSubtitleStreams());
|
||||
setOutputIndexes(sortedSubtitles);
|
||||
modules.add(new MapAllModule<>(sortedSubtitles));
|
||||
modules.add(new SetDefaultStreamModule<>(sortedSubtitles, 0));
|
||||
modules.add(new SetStreamLanguageModule<>(sortedSubtitles));
|
||||
OutputUtil.printDebug("Subtitle preference: " + this.subtitleLanguages + ", Subtitle name filter: " +
|
||||
this.subtitleNameFilter + ", Minimal Subtitle Preference: " + this.subtitlePreference.name() +
|
||||
", Sorted order: " + Arrays.toString(sortedSubtitles.toArray()));
|
||||
|
||||
//Get the first video stream
|
||||
VideoStream videoStream = getFirstVideoStream(streams);
|
||||
if (configuration.useHardwareDecoding()) {
|
||||
modules.add(new HardwareDecodeModule());
|
||||
}
|
||||
|
||||
//Add streams to output file
|
||||
FFMpegHelper.addAudioStream(command, audioStream, toStereo);
|
||||
FFMpegHelper.addSubtitleAndVideoStream(command, subtitleStream, videoStream, file);
|
||||
boolean encodeFlac = this.forceAudioEncoding || FFMpegConvert.getConfiguration().alwaysEncodeFlac();
|
||||
for (AudioStream audioStream : sortedAudio) {
|
||||
if (!encodeFlac || !audioStream.getCodecName().equalsIgnoreCase("flac")) {
|
||||
modules.add(new CopyAudioModule(audioStream));
|
||||
}
|
||||
}
|
||||
modules.add(new CopySubtitlesModule());
|
||||
|
||||
command.add(outFile);
|
||||
return command.toArray(new String[0]);
|
||||
boolean encodingNecessary = false;
|
||||
for (VideoStream videoStream : probeResult.getVideoStreams()) {
|
||||
if (!videoStream.getCodecName().trim().equalsIgnoreCase("hevc")) {
|
||||
encodingNecessary = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (encodingNecessary || this.forceVideoEncoding) {
|
||||
List<String> availableHWAcceleration = getAvailableHardwareEncodingMethods();
|
||||
if (configuration.useHardwareEncoding() && availableHWAcceleration.contains("qsv")) {
|
||||
modules.add(new SetVideoCodecModule("hevc_qsv"));
|
||||
modules.add(new SetQualityModule(17, "veryslow"));
|
||||
} else if (configuration.useHardwareEncoding() && availableHWAcceleration.contains("cuda")) {
|
||||
modules.add(new H265HardwareEncodingModule(20));
|
||||
} else {
|
||||
modules.add(new SetVideoCodecModule("hevc"));
|
||||
modules.add(new SetQualityModule(19, "medium"));
|
||||
}
|
||||
} else {
|
||||
modules.add(new CopyVideoModule());
|
||||
}
|
||||
|
||||
// Map all attached streams, such as fonts and covers
|
||||
modules.add(new MapAllModule<>(probeResult.getOtherStreams()));
|
||||
|
||||
modules.add(new SetOutputFileModule(outFile));
|
||||
|
||||
new ModuleExecutor(command, modules).execute();
|
||||
|
||||
int index = videoStreams.size();
|
||||
for (OtherStream stream : probeResult.getOtherStreams()) {
|
||||
if (stream.isCoverImage()) {
|
||||
command.addOutputFileOption("-c:v:" + index++, "copy");
|
||||
}
|
||||
}
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getValidFormats() {
|
||||
return videoFormats;
|
||||
@NotNull
|
||||
public List<String> getValidFormats() {
|
||||
return this.videoFormats;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,10 +1,17 @@
|
||||
package net.knarcraft.ffmpegconverter.converter;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.streams.AudioStream;
|
||||
import net.knarcraft.ffmpegconverter.streams.StreamObject;
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.DebugModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ModuleExecutor;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.mapping.NthAudioStreamModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
|
||||
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@ -19,29 +26,35 @@ public class AudioConverter extends AbstractConverter {
|
||||
* @param ffmpegPath <p>Path/command to ffmpeg.</p>
|
||||
* @param newExtension <p>The extension of the new file.</p>
|
||||
*/
|
||||
public AudioConverter(String ffprobePath, String ffmpegPath, String newExtension) {
|
||||
public AudioConverter(@NotNull String ffprobePath, @NotNull String ffmpegPath, @NotNull String newExtension) {
|
||||
super(newExtension);
|
||||
this.ffprobePath = ffprobePath;
|
||||
this.ffmpegPath = ffmpegPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] generateConversionCommand(String executable, File file, List<StreamObject> streams, String outFile) {
|
||||
List<String> command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, file.getName());
|
||||
@Nullable
|
||||
public FFMpegCommand generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
|
||||
@NotNull String outFile) {
|
||||
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
|
||||
List<ConverterModule> modules = new ArrayList<>();
|
||||
|
||||
if (this.debug) {
|
||||
FFMpegHelper.addDebugArguments(command, 50, 120);
|
||||
modules.add(new DebugModule());
|
||||
}
|
||||
|
||||
//Gets the first audio stream from the file and adds it to the output file
|
||||
AudioStream audioStream = getFirstAudioSteam(streams);
|
||||
FFMpegHelper.addAudioStream(command, audioStream, false);
|
||||
command.add(outFile);
|
||||
modules.add(new NthAudioStreamModule(probeResult.getAudioStreams(), 0));
|
||||
modules.add(new SetOutputFileModule(outFile));
|
||||
|
||||
return command.toArray(new String[0]);
|
||||
new ModuleExecutor(command, modules).execute();
|
||||
return command;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getValidFormats() {
|
||||
@NotNull
|
||||
public List<String> getValidFormats() {
|
||||
return audioFormats;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
package net.knarcraft.ffmpegconverter.converter;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.DebugModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ModuleExecutor;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.mapping.NthAudioStreamModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
|
||||
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An extractor for getting audio streams from video files
|
||||
*/
|
||||
public class AudioExtractor extends AbstractConverter {
|
||||
|
||||
private final int streamToExtract;
|
||||
|
||||
/**
|
||||
* Instantiates a new audio extractor
|
||||
*
|
||||
* @param ffprobePath <p>Path/command to ffprobe.</p>
|
||||
* @param ffmpegPath <p>Path/command to ffmpeg.</p>
|
||||
* @param newExtension <p>The extension of the new file.</p>
|
||||
* @param streamToExtract <p>The stream to be extracted from the video file</p>
|
||||
*/
|
||||
public AudioExtractor(@NotNull String ffprobePath, @NotNull String ffmpegPath, @NotNull String newExtension,
|
||||
int streamToExtract) {
|
||||
super(newExtension);
|
||||
this.ffprobePath = ffprobePath;
|
||||
this.ffmpegPath = ffmpegPath;
|
||||
this.streamToExtract = Math.max(0, streamToExtract);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public FFMpegCommand generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
|
||||
@NotNull String outFile) {
|
||||
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
|
||||
List<ConverterModule> modules = new ArrayList<>();
|
||||
|
||||
if (this.debug) {
|
||||
modules.add(new DebugModule());
|
||||
}
|
||||
|
||||
//Gets the first audio stream from the file and adds it to the output file
|
||||
modules.add(new NthAudioStreamModule(probeResult.getAudioStreams(), streamToExtract));
|
||||
modules.add(new SetOutputFileModule(outFile));
|
||||
|
||||
new ModuleExecutor(command, modules).execute();
|
||||
return command;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public List<String> getValidFormats() {
|
||||
return videoFormats;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
package net.knarcraft.ffmpegconverter.converter;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.DebugModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ModuleExecutor;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.mapping.MapAllModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.CopySubtitlesModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.CopyVideoModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
|
||||
import net.knarcraft.ffmpegconverter.streams.AudioStream;
|
||||
import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
|
||||
import net.knarcraft.ffmpegconverter.streams.VideoStream;
|
||||
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A converter to convert a video's audio into flac
|
||||
*/
|
||||
public class AudioToVorbisConverter extends AbstractConverter {
|
||||
|
||||
/**
|
||||
* Initializes variables used by the abstract converter
|
||||
*
|
||||
* @param ffprobePath <p>Path/command to ffprobe.</p>
|
||||
* @param ffmpegPath <p>Path/command to ffmpeg.</p>
|
||||
*/
|
||||
public AudioToVorbisConverter(String ffprobePath, String ffmpegPath) {
|
||||
super("mkv");
|
||||
this.ffprobePath = ffprobePath;
|
||||
this.ffmpegPath = ffmpegPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<String> getValidFormats() {
|
||||
return this.videoFormats;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable FFMpegCommand generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult, @NotNull String outFile) {
|
||||
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
|
||||
List<ConverterModule> modules = new ArrayList<>();
|
||||
|
||||
if (this.debug) {
|
||||
modules.add(new DebugModule());
|
||||
}
|
||||
|
||||
// Map video if present
|
||||
List<VideoStream> videoStreams = probeResult.getVideoStreams();
|
||||
if (!videoStreams.isEmpty()) {
|
||||
modules.add(new MapAllModule<>(videoStreams));
|
||||
modules.add(new CopyVideoModule());
|
||||
}
|
||||
|
||||
// Map audio if present
|
||||
List<AudioStream> audioStreams = probeResult.getAudioStreams();
|
||||
if (!audioStreams.isEmpty()) {
|
||||
modules.add(new MapAllModule<>(audioStreams));
|
||||
setOutputIndexes(audioStreams);
|
||||
}
|
||||
|
||||
// Map subtitles if present
|
||||
List<SubtitleStream> subtitleStreams = probeResult.getSubtitleStreams();
|
||||
if (!subtitleStreams.isEmpty()) {
|
||||
modules.add(new MapAllModule<>(subtitleStreams));
|
||||
setOutputIndexes(subtitleStreams);
|
||||
modules.add(new CopySubtitlesModule());
|
||||
}
|
||||
|
||||
// Map any fonts, cover images or similar
|
||||
modules.add(new MapAllModule<>(probeResult.getOtherStreams()));
|
||||
|
||||
// Set output file and execute
|
||||
modules.add(new SetOutputFileModule(outFile));
|
||||
new ModuleExecutor(command, modules).execute();
|
||||
return command;
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +1,9 @@
|
||||
package net.knarcraft.ffmpegconverter.converter;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.streams.StreamObject;
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@ -9,7 +12,15 @@ import java.util.List;
|
||||
/**
|
||||
* This interface describes a file converter
|
||||
*/
|
||||
interface Converter {
|
||||
public interface Converter {
|
||||
|
||||
/**
|
||||
* Gets all valid input formats for the converter
|
||||
*
|
||||
* @return <p>A list of valid input formats</p>
|
||||
*/
|
||||
@NotNull
|
||||
List<String> getValidFormats();
|
||||
|
||||
/**
|
||||
* Converts the given file
|
||||
@ -17,16 +28,18 @@ interface Converter {
|
||||
* @param file <p>The file to convert.</p>
|
||||
* @throws IOException <p>If the file cannot be converted.</p>
|
||||
*/
|
||||
void convert(File file) throws IOException;
|
||||
void convert(@NotNull File file) throws IOException;
|
||||
|
||||
/**
|
||||
* Generates a command for a ProcessBuilder.
|
||||
*
|
||||
* @param executable <p>The executable file for ffmpeg.</p>
|
||||
* @param file <p>The input file.</p>
|
||||
* @param streams <p>A list of ffprobe streams.</p>
|
||||
* @param outFile <p>The output file.</p>
|
||||
* @param executable <p>The executable file for ffmpeg</p>
|
||||
* @param probeResult <p>The result of probing the input file</p>
|
||||
* @param outFile <p>The output file</p>
|
||||
* @return <p>A list of commands</p>
|
||||
*/
|
||||
String[] generateConversionCommand(String executable, File file, List<StreamObject> streams, String outFile);
|
||||
@Nullable
|
||||
FFMpegCommand generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
|
||||
@NotNull String outFile);
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,96 @@
|
||||
package net.knarcraft.ffmpegconverter.converter;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.DebugModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ModuleExecutor;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.hardwarecoding.HardwareDecodeModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.mapping.MapAllModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.CopyAudioModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.CopySubtitlesModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.ScaleModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.SetQualityModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.SetVideoCodecModule;
|
||||
import net.knarcraft.ffmpegconverter.streams.StreamObject;
|
||||
import net.knarcraft.ffmpegconverter.streams.VideoStream;
|
||||
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A converter for converting video files
|
||||
*/
|
||||
public class DownScaleConverter extends AbstractConverter {
|
||||
|
||||
private final int newWidth;
|
||||
private final int newHeight;
|
||||
|
||||
/**
|
||||
* Instantiates a new video converter
|
||||
*
|
||||
* @param ffprobePath <p>Path/command to ffprobe.</p>
|
||||
* @param ffmpegPath <p>Path/command to ffmpeg.</p>
|
||||
* @param newWidth <p>The new width of the video</p>
|
||||
* @param newHeight <p>The new height of the video</p>
|
||||
*/
|
||||
public DownScaleConverter(@NotNull String ffprobePath, @NotNull String ffmpegPath, int newWidth, int newHeight) {
|
||||
super(null);
|
||||
this.ffprobePath = ffprobePath;
|
||||
this.ffmpegPath = ffmpegPath;
|
||||
this.newHeight = newHeight;
|
||||
this.newWidth = newWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public FFMpegCommand generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
|
||||
@NotNull String outFile) {
|
||||
List<StreamObject> streams = probeResult.parsedStreams();
|
||||
List<ConverterModule> modules = new ArrayList<>();
|
||||
VideoStream videoStream = FFMpegHelper.getNthSteam(probeResult.getVideoStreams(), 0);
|
||||
|
||||
if (videoStream == null) {
|
||||
throw new IllegalArgumentException("The selected file has no video stream");
|
||||
}
|
||||
|
||||
// Simply ignore files below, or at, the target resolution
|
||||
if (videoStream.getWidth() <= this.newWidth && videoStream.getHeight() <= this.newHeight) {
|
||||
return null;
|
||||
}
|
||||
|
||||
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
|
||||
if (this.debug) {
|
||||
modules.add(new DebugModule());
|
||||
}
|
||||
|
||||
//Add all streams without re-encoding
|
||||
modules.add(new MapAllModule<>(streams));
|
||||
modules.add(new CopyAudioModule());
|
||||
modules.add(new CopySubtitlesModule());
|
||||
|
||||
if (videoStream.getCodecName().trim().equalsIgnoreCase("hevc")) {
|
||||
modules.add(new SetVideoCodecModule("hevc"));
|
||||
} else {
|
||||
modules.add(new SetVideoCodecModule("h264"));
|
||||
}
|
||||
|
||||
modules.add(new ScaleModule(this.newWidth, this.newHeight));
|
||||
modules.add(new SetQualityModule(20, "slow"));
|
||||
modules.add(new SetOutputFileModule(outFile));
|
||||
modules.add(new HardwareDecodeModule());
|
||||
new ModuleExecutor(command, modules).execute();
|
||||
return command;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public List<String> getValidFormats() {
|
||||
return videoFormats;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,197 @@
|
||||
package net.knarcraft.ffmpegconverter.converter;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.container.ProcessResult;
|
||||
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.DebugModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ModuleExecutor;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.mapping.MapAllModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.CopyAudioModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.CopySubtitlesModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
|
||||
import net.knarcraft.ffmpegconverter.streams.AudioStream;
|
||||
import net.knarcraft.ffmpegconverter.streams.StreamObject;
|
||||
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
||||
import net.knarcraft.ffmpegconverter.utility.StringUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A converter that crops letter-boxing (black padding) from video files
|
||||
*/
|
||||
public class LetterboxCropper extends AbstractConverter {
|
||||
|
||||
private final static String SPACER = "|,-,|";
|
||||
|
||||
/**
|
||||
* Initializes a new letter box cropper
|
||||
*
|
||||
* @param ffprobePath <p>Path/command to ffprobe</p>
|
||||
* @param ffmpegPath <p>Path/command to ffmpeg</p>
|
||||
*/
|
||||
public LetterboxCropper(@NotNull String ffprobePath, @NotNull String ffmpegPath) {
|
||||
super("mkv");
|
||||
this.ffprobePath = ffprobePath;
|
||||
this.ffmpegPath = ffmpegPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<String> getValidFormats() {
|
||||
return List.of("mkv", "mp4");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public FFMpegCommand generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
|
||||
@NotNull String outFile) {
|
||||
File inputFile = probeResult.parsedFiles().get(0);
|
||||
|
||||
List<ConverterModule> modules = new ArrayList<>();
|
||||
|
||||
Map<String, Integer> cropValues = getLetterboxCropValues(modules, inputFile);
|
||||
|
||||
FFMpegCommand convertCommand = getConversionCommand(cropValues, inputFile);
|
||||
|
||||
modules.add(new MapAllModule<>(probeResult.getVideoStreams()));
|
||||
|
||||
// Map audio if present
|
||||
List<AudioStream> audioStreams = probeResult.getAudioStreams();
|
||||
if (!audioStreams.isEmpty()) {
|
||||
modules.add(new MapAllModule<>(audioStreams));
|
||||
setOutputIndexes(audioStreams);
|
||||
modules.add(new CopyAudioModule(audioStreams));
|
||||
}
|
||||
|
||||
// Map subtitles if present
|
||||
List<StreamObject> subtitleStreams = new ArrayList<>(probeResult.getSubtitleStreams());
|
||||
if (!subtitleStreams.isEmpty()) {
|
||||
modules.add(new MapAllModule<>(probeResult.getSubtitleStreams()));
|
||||
setOutputIndexes(subtitleStreams);
|
||||
modules.add(new CopySubtitlesModule());
|
||||
}
|
||||
|
||||
// Map all attached streams, such as fonts and covers
|
||||
modules.add(new MapAllModule<>(probeResult.getOtherStreams()));
|
||||
|
||||
modules.add(new SetOutputFileModule(outFile));
|
||||
|
||||
new ModuleExecutor(convertCommand, modules).execute();
|
||||
return convertCommand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the initial command used for removing the letterbox from a video file
|
||||
*
|
||||
* @param cropValues <p>The possible crop values to be used</p>
|
||||
* @param inputFile <p>The file to be converted</p>
|
||||
* @return <p>The initial command to use for converting the file</p>
|
||||
*/
|
||||
@NotNull
|
||||
private FFMpegCommand getConversionCommand(@NotNull Map<String, Integer> cropValues, @NotNull File inputFile) {
|
||||
String crop = null;
|
||||
int counts = 0;
|
||||
|
||||
for (Map.Entry<String, Integer> entry : cropValues.entrySet()) {
|
||||
if (crop == null || entry.getValue() > counts) {
|
||||
crop = entry.getKey();
|
||||
counts = entry.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
if (crop == null) {
|
||||
throw new IllegalArgumentException("Unable to detect letterbox for video");
|
||||
}
|
||||
|
||||
FFMpegCommand convertCommand = new FFMpegCommand(ffmpegPath);
|
||||
|
||||
convertCommand.addInputFile(inputFile.getName());
|
||||
convertCommand.addOutputFileOption("-vf", "crop=" + crop);
|
||||
convertCommand.addOutputFileOption("-c:v", "libx265");
|
||||
convertCommand.addOutputFileOption("-crf", "22");
|
||||
convertCommand.addOutputFileOption("-preset", "medium");
|
||||
convertCommand.addOutputFileOption("-tune", "ssim");
|
||||
convertCommand.addOutputFileOption("-profile:v", "main10");
|
||||
convertCommand.addOutputFileOption("-level:v", "4");
|
||||
convertCommand.addOutputFileOption("-x265-params", "bframes=8:scenecut-bias=0.05:me=star:subme=5:" +
|
||||
"refine-mv=1:limit-refs=1:rskip=0:max-merge=5:rc-lookahead=80:lookahead-slices=5:min-keyint=23:" +
|
||||
"max-luma=1023:psy-rd=2:strong-intra-smoothing=0:b-intra=1:hist-threshold=0.03");
|
||||
return convertCommand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a map counting each time a given letterbox crop suggestion has been encountered
|
||||
*
|
||||
* <p>As some parts of a video might be entirely black, or some parts of a video might be dark enough that parts of
|
||||
* a scene might be detected as part of the letterbox, this takes 30 crop-detect samples evenly spread among the
|
||||
* file. It can be assumed that the crop-detect value that was encountered the most times is the correct one.</p>
|
||||
*
|
||||
* @param modules <p>The converter modules to append to</p>
|
||||
* @param inputFile <p>The input file to find letterbox of</p>
|
||||
* @return <p>A map counting all returned crop-detect values</p>
|
||||
*/
|
||||
@NotNull
|
||||
private Map<String, Integer> getLetterboxCropValues(@NotNull List<ConverterModule> modules, @NotNull File inputFile) {
|
||||
Map<String, Integer> cropValues = new HashMap<>();
|
||||
|
||||
if (this.debug) {
|
||||
modules.add(new DebugModule());
|
||||
}
|
||||
|
||||
FFMpegCommand probeCommand = new FFMpegCommand(ffmpegPath);
|
||||
probeCommand.addGlobalOption("-nostats", "-hide_banner");
|
||||
probeCommand.addInputFile(inputFile.toString());
|
||||
probeCommand.addOutputFileOption("-vframes", "10");
|
||||
probeCommand.addOutputFileOption("-vf", "cropdetect");
|
||||
probeCommand.addOutputFileOption("-f", "null");
|
||||
probeCommand.addOutputFileOption("-");
|
||||
|
||||
double duration;
|
||||
try {
|
||||
duration = FFMpegHelper.getDuration(ffprobePath, inputFile);
|
||||
} catch (IOException | NumberFormatException exception) {
|
||||
throw new RuntimeException("Unable to get duration from video file");
|
||||
}
|
||||
|
||||
int increments = (int) (duration / 30d);
|
||||
|
||||
for (int i = 0; i < duration; i += increments) {
|
||||
FFMpegCommand clone = probeCommand.clone();
|
||||
clone.addInputFileOption("-ss", String.valueOf(i));
|
||||
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(clone.getResult());
|
||||
ProcessResult result = null;
|
||||
try {
|
||||
result = FFMpegHelper.runProcess(processBuilder, inputFile.getParentFile(), SPACER, false);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
if (result == null || result.exitCode() != 0) {
|
||||
throw new IllegalArgumentException("File probe failed with code " + (result == null ? "null" :
|
||||
result.exitCode()));
|
||||
}
|
||||
|
||||
List<String> parsed = StringUtil.stringBetween(result.output(), "crop=", SPACER);
|
||||
for (String string : parsed) {
|
||||
if (string.contains("-")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cropValues.containsKey(string)) {
|
||||
cropValues.put(string, cropValues.get(string) + 1);
|
||||
} else {
|
||||
cropValues.put(string, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cropValues;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package net.knarcraft.ffmpegconverter.converter;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.DebugModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ModuleExecutor;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.hardwarecoding.HardwareDecodeModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.mapping.NthAudioStreamModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.mapping.NthSubtitleStreamModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.mapping.NthVideoStreamModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.CopyAllModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
|
||||
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A transcoder which takes one of each stream from an MKV file and produces an MP4 file
|
||||
*/
|
||||
public class MKVToMP4Transcoder extends AbstractConverter {
|
||||
|
||||
private final int videoStreamIndex;
|
||||
private final int audioStreamIndex;
|
||||
private final int subtitleStreamIndex;
|
||||
|
||||
/**
|
||||
* Instantiates a new mkv to mp4 transcoder
|
||||
*
|
||||
* @param ffprobePath <p>Path/command to ffprobe.</p>
|
||||
* @param ffmpegPath <p>Path/command to ffmpeg.</p>
|
||||
* @param audioStreamIndex <p>The relative index of the audio stream to use (0 or below selects the first)</p>
|
||||
* @param subtitleStreamIndex <p>The relative index of the subtitle stream to use (0 or below selects the first)</p>
|
||||
* @param videoStreamIndex <p>The relative index of the video stream to use (0 or below selects the first)</p>
|
||||
*/
|
||||
public MKVToMP4Transcoder(String ffprobePath, String ffmpegPath, int audioStreamIndex, int subtitleStreamIndex,
|
||||
int videoStreamIndex) {
|
||||
super("mp4");
|
||||
this.ffprobePath = ffprobePath;
|
||||
this.ffmpegPath = ffmpegPath;
|
||||
this.videoStreamIndex = videoStreamIndex;
|
||||
this.audioStreamIndex = audioStreamIndex;
|
||||
this.subtitleStreamIndex = subtitleStreamIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public List<String> getValidFormats() {
|
||||
return List.of("mkv");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public FFMpegCommand generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
|
||||
@NotNull String outFile) {
|
||||
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
|
||||
List<ConverterModule> modules = new ArrayList<>();
|
||||
|
||||
if (this.debug) {
|
||||
modules.add(new DebugModule());
|
||||
}
|
||||
|
||||
// Copy stream info
|
||||
modules.add(new CopyAllModule());
|
||||
|
||||
//Add streams to output file
|
||||
if (!probeResult.getAudioStreams().isEmpty()) {
|
||||
modules.add(new NthAudioStreamModule(probeResult.getAudioStreams(), this.audioStreamIndex));
|
||||
}
|
||||
if (!probeResult.getVideoStreams().isEmpty()) {
|
||||
modules.add(new NthVideoStreamModule(probeResult.getVideoStreams(), this.videoStreamIndex));
|
||||
modules.add(new HardwareDecodeModule());
|
||||
}
|
||||
if (!probeResult.getSubtitleStreams().isEmpty()) {
|
||||
modules.add(new NthSubtitleStreamModule(probeResult.getSubtitleStreams(), this.subtitleStreamIndex));
|
||||
}
|
||||
|
||||
modules.add(new SetOutputFileModule(outFile));
|
||||
|
||||
new ModuleExecutor(command, modules).execute();
|
||||
return command;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
package net.knarcraft.ffmpegconverter.converter;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.DebugModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ModuleExecutor;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.hardwarecoding.H264HardwareEncodingModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.hardwarecoding.HardwareDecodeModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.mapping.MapAllModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.CopyAudioModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.CopySubtitlesModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.FastStartModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
|
||||
import net.knarcraft.ffmpegconverter.streams.AudioStream;
|
||||
import net.knarcraft.ffmpegconverter.streams.StreamObject;
|
||||
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A converter solely for the purpose of converting video streams of MKV files into h264
|
||||
*/
|
||||
public class MkvH264Converter extends AbstractConverter {
|
||||
|
||||
/**
|
||||
* Initializes variables used by the abstract converter
|
||||
*
|
||||
* @param ffprobePath <p>Path/command to ffprobe.</p>
|
||||
* @param ffmpegPath <p>Path/command to ffmpeg.</p>
|
||||
*/
|
||||
public MkvH264Converter(@NotNull String ffprobePath, @NotNull String ffmpegPath) {
|
||||
super("mkv");
|
||||
this.ffprobePath = ffprobePath;
|
||||
this.ffmpegPath = ffmpegPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public List<String> getValidFormats() {
|
||||
return List.of("mkv");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public FFMpegCommand generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
|
||||
@NotNull String outFile) {
|
||||
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
|
||||
List<ConverterModule> modules = new ArrayList<>();
|
||||
|
||||
if (this.debug) {
|
||||
modules.add(new DebugModule());
|
||||
}
|
||||
|
||||
// Map video if present
|
||||
List<StreamObject> videoStreams = new ArrayList<>(probeResult.getVideoStreams());
|
||||
if (!videoStreams.isEmpty()) {
|
||||
modules.add(new HardwareDecodeModule());
|
||||
modules.add(new MapAllModule<>(videoStreams));
|
||||
modules.add(new H264HardwareEncodingModule(17));
|
||||
modules.add(new FastStartModule());
|
||||
}
|
||||
|
||||
// Map audio if present
|
||||
List<AudioStream> audioStreams = probeResult.getAudioStreams();
|
||||
if (!audioStreams.isEmpty()) {
|
||||
modules.add(new MapAllModule<>(audioStreams));
|
||||
setOutputIndexes(audioStreams);
|
||||
modules.add(new CopyAudioModule(audioStreams));
|
||||
}
|
||||
|
||||
// Map subtitles if present
|
||||
List<StreamObject> subtitleStreams = new ArrayList<>(probeResult.getSubtitleStreams());
|
||||
if (!subtitleStreams.isEmpty()) {
|
||||
modules.add(new MapAllModule<>(probeResult.getSubtitleStreams()));
|
||||
setOutputIndexes(subtitleStreams);
|
||||
modules.add(new CopySubtitlesModule());
|
||||
}
|
||||
|
||||
modules.add(new SetOutputFileModule(outFile));
|
||||
new ModuleExecutor(command, modules).execute();
|
||||
return command;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
package net.knarcraft.ffmpegconverter.converter;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.DebugModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ModuleExecutor;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.hardwarecoding.H265HardwareEncodingModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.hardwarecoding.HardwareDecodeModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.mapping.MapAllModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.CopySubtitlesModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.FastStartModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
|
||||
import net.knarcraft.ffmpegconverter.streams.AudioStream;
|
||||
import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
|
||||
import net.knarcraft.ffmpegconverter.streams.VideoStream;
|
||||
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A converter solely for the purpose of converting video streams of MKV files into h264
|
||||
*/
|
||||
public class MkvH265ReducedConverter extends AbstractConverter {
|
||||
|
||||
/**
|
||||
* Initializes variables used by the abstract converter
|
||||
*
|
||||
* @param ffprobePath <p>Path/command to ffprobe.</p>
|
||||
* @param ffmpegPath <p>Path/command to ffmpeg.</p>
|
||||
*/
|
||||
public MkvH265ReducedConverter(String ffprobePath, String ffmpegPath) {
|
||||
super("mkv");
|
||||
this.ffprobePath = ffprobePath;
|
||||
this.ffmpegPath = ffmpegPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public List<String> getValidFormats() {
|
||||
return List.of("mkv");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public FFMpegCommand generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
|
||||
@NotNull String outFile) {
|
||||
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
|
||||
List<ConverterModule> modules = new ArrayList<>();
|
||||
|
||||
if (this.debug) {
|
||||
modules.add(new DebugModule());
|
||||
}
|
||||
|
||||
// Map video if present
|
||||
List<VideoStream> videoStreams = probeResult.getVideoStreams();
|
||||
if (!videoStreams.isEmpty()) {
|
||||
modules.add(new HardwareDecodeModule());
|
||||
modules.add(new H265HardwareEncodingModule(19));
|
||||
modules.add(new FastStartModule());
|
||||
modules.add(new MapAllModule<>(videoStreams));
|
||||
}
|
||||
|
||||
// Map audio if present
|
||||
List<AudioStream> audioStreams = probeResult.getAudioStreams();
|
||||
if (!audioStreams.isEmpty()) {
|
||||
modules.add(new MapAllModule<>(audioStreams));
|
||||
setOutputIndexes(audioStreams);
|
||||
command.addOutputFileOption("-c:a", "flac");
|
||||
command.addOutputFileOption("-compression_level", "12");
|
||||
}
|
||||
|
||||
// Map subtitles if present
|
||||
List<SubtitleStream> subtitleStreams = probeResult.getSubtitleStreams();
|
||||
if (!subtitleStreams.isEmpty()) {
|
||||
modules.add(new MapAllModule<>(subtitleStreams));
|
||||
modules.add(new CopySubtitlesModule());
|
||||
}
|
||||
|
||||
// Map any fonts, cover images or similar
|
||||
modules.add(new MapAllModule<>(probeResult.getOtherStreams()));
|
||||
|
||||
// Set output file and execute
|
||||
modules.add(new SetOutputFileModule(outFile));
|
||||
new ModuleExecutor(command, modules).execute();
|
||||
return command;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
package net.knarcraft.ffmpegconverter.converter;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.FFMpegConvert;
|
||||
import net.knarcraft.ffmpegconverter.config.Configuration;
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.DebugModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ModuleExecutor;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.mapping.MapAllModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.mapping.SetDefaultStreamModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.CopyAudioModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.CopySubtitlesModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.CopyVideoModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.FastStartModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.SetStreamLanguageModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.sorter.AudioLanguageSorter;
|
||||
import net.knarcraft.ffmpegconverter.converter.sorter.MinimalSubtitleSorter;
|
||||
import net.knarcraft.ffmpegconverter.converter.sorter.SpecialAudioSorter;
|
||||
import net.knarcraft.ffmpegconverter.converter.sorter.StreamSorter;
|
||||
import net.knarcraft.ffmpegconverter.converter.sorter.SubtitleLanguageSorter;
|
||||
import net.knarcraft.ffmpegconverter.converter.sorter.SubtitleTitleSorter;
|
||||
import net.knarcraft.ffmpegconverter.property.MinimalSubtitlePreference;
|
||||
import net.knarcraft.ffmpegconverter.streams.AudioStream;
|
||||
import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
|
||||
import net.knarcraft.ffmpegconverter.streams.VideoStream;
|
||||
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
||||
import net.knarcraft.ffmpegconverter.utility.OutputUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A converter for converting anime, keeping all streams
|
||||
*/
|
||||
public class StreamOrderConverter extends AbstractConverter {
|
||||
|
||||
private final List<String> audioLanguages;
|
||||
private final List<String> subtitleLanguages;
|
||||
private final MinimalSubtitlePreference subtitlePreference;
|
||||
private final String subtitleNameFilter;
|
||||
|
||||
/**
|
||||
* Instantiates a new anime converter
|
||||
*
|
||||
* @param ffprobePath <p>Path/command to ffprobe.</p>
|
||||
* @param ffmpegPath <p>Path/command to ffmpeg.</p>
|
||||
* @param audioLanguages <p>The language priority for languages</p>
|
||||
* @param subtitleLanguages <p>The language priority for subtitles</p>
|
||||
*/
|
||||
public StreamOrderConverter(@NotNull String ffprobePath, @NotNull String ffmpegPath,
|
||||
@NotNull String audioLanguages, @NotNull String subtitleLanguages,
|
||||
@NotNull String subtitleNameFilter) {
|
||||
super("mkv");
|
||||
Configuration configuration = FFMpegConvert.getConfiguration();
|
||||
this.ffprobePath = ffprobePath;
|
||||
this.ffmpegPath = ffmpegPath;
|
||||
this.audioLanguages = List.of(audioLanguages.split(","));
|
||||
this.subtitleLanguages = List.of(subtitleLanguages.split(","));
|
||||
this.subtitlePreference = configuration.getMinimalSubtitlePreference();
|
||||
this.subtitleNameFilter = subtitleNameFilter;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public FFMpegCommand generateConversionCommand(@NotNull String executable,
|
||||
@NotNull StreamProbeResult probeResult,
|
||||
@NotNull String outFile) {
|
||||
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
|
||||
List<ConverterModule> modules = new ArrayList<>();
|
||||
if (this.debug) {
|
||||
modules.add(new DebugModule());
|
||||
}
|
||||
modules.add(new FastStartModule());
|
||||
|
||||
//Map all video streams
|
||||
List<VideoStream> videoStreams = probeResult.getVideoStreams();
|
||||
modules.add(new MapAllModule<>(videoStreams));
|
||||
setOutputIndexes(videoStreams);
|
||||
|
||||
//Get the first audio stream in accordance with chosen languages
|
||||
StreamSorter<AudioStream> audioSorter = new AudioLanguageSorter(this.audioLanguages)
|
||||
.append(new SpecialAudioSorter(MinimalSubtitlePreference.REJECT));
|
||||
List<AudioStream> sortedAudio = audioSorter.chainSort(probeResult.getAudioStreams());
|
||||
setOutputIndexes(sortedAudio);
|
||||
modules.add(new MapAllModule<>(sortedAudio));
|
||||
modules.add(new SetDefaultStreamModule<>(sortedAudio, 0));
|
||||
modules.add(new SetStreamLanguageModule<>(sortedAudio));
|
||||
|
||||
//Get the first subtitle stream in accordance with chosen languages and signs and songs prevention
|
||||
StreamSorter<SubtitleStream> subtitleSorter = new SubtitleTitleSorter(
|
||||
List.of(this.subtitleNameFilter.split(",")))
|
||||
.append(new SubtitleLanguageSorter(this.subtitleLanguages))
|
||||
.append(new MinimalSubtitleSorter(this.subtitlePreference));
|
||||
List<SubtitleStream> sortedSubtitles = subtitleSorter.chainSort(probeResult.getSubtitleStreams());
|
||||
setOutputIndexes(sortedSubtitles);
|
||||
modules.add(new MapAllModule<>(sortedSubtitles));
|
||||
modules.add(new SetDefaultStreamModule<>(sortedSubtitles, 0));
|
||||
modules.add(new SetStreamLanguageModule<>(sortedSubtitles));
|
||||
OutputUtil.printDebug("Subtitle preference: " + this.subtitleLanguages + ", Subtitle name filter: " +
|
||||
this.subtitleNameFilter + ", Minimal Subtitle Preference: " + this.subtitlePreference.name() +
|
||||
", Sorted order: " + Arrays.toString(sortedSubtitles.toArray()));
|
||||
|
||||
for (AudioStream audioStream : sortedAudio) {
|
||||
modules.add(new CopyAudioModule(audioStream));
|
||||
}
|
||||
modules.add(new CopySubtitlesModule());
|
||||
modules.add(new CopyVideoModule());
|
||||
|
||||
// Map all attached streams, such as fonts and covers
|
||||
modules.add(new MapAllModule<>(probeResult.getOtherStreams()));
|
||||
|
||||
modules.add(new SetOutputFileModule(outFile));
|
||||
|
||||
new ModuleExecutor(command, modules).execute();
|
||||
return command;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public List<String> getValidFormats() {
|
||||
return this.videoFormats;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
package net.knarcraft.ffmpegconverter.converter;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.DebugModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ModuleExecutor;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.hardwarecoding.HardwareDecodeModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.mapping.MapAllModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.CopyAudioModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.CopyVideoModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.MovTextModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
|
||||
import net.knarcraft.ffmpegconverter.streams.AudioStream;
|
||||
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A converter to embed adjacent subtitles into mp4 files
|
||||
*/
|
||||
public class SubtitleEmbed extends AbstractConverter {
|
||||
|
||||
/**
|
||||
* Initializes variables used by the abstract converter
|
||||
*
|
||||
* @param ffprobePath <p>Path/command to ffprobe.</p>
|
||||
* @param ffmpegPath <p>Path/command to ffmpeg.</p>
|
||||
*/
|
||||
public SubtitleEmbed(String ffprobePath, String ffmpegPath) {
|
||||
super(null);
|
||||
this.ffprobePath = ffprobePath;
|
||||
this.ffmpegPath = ffmpegPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public List<String> getValidFormats() {
|
||||
return List.of("mp4");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public FFMpegCommand generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
|
||||
@NotNull String outFile) {
|
||||
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
|
||||
List<ConverterModule> modules = new ArrayList<>();
|
||||
|
||||
if (this.debug) {
|
||||
modules.add(new DebugModule());
|
||||
}
|
||||
|
||||
modules.add(new MapAllModule<>(probeResult.parsedStreams()));
|
||||
modules.add(new HardwareDecodeModule());
|
||||
List<AudioStream> audioStreams = probeResult.getAudioStreams();
|
||||
setOutputIndexes(audioStreams);
|
||||
modules.add(new CopyAudioModule(audioStreams));
|
||||
modules.add(new CopyVideoModule());
|
||||
modules.add(new MovTextModule());
|
||||
modules.add(new SetOutputFileModule(outFile));
|
||||
|
||||
new ModuleExecutor(command, modules).execute();
|
||||
return command;
|
||||
}
|
||||
|
||||
}
|
@ -1,9 +1,20 @@
|
||||
package net.knarcraft.ffmpegconverter.converter;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.DebugModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ModuleExecutor;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.hardwarecoding.HardwareDecodeModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.mapping.MapAllModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.CopyAllModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
|
||||
import net.knarcraft.ffmpegconverter.streams.StreamObject;
|
||||
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@ -25,24 +36,31 @@ public class VideoConverter extends AbstractConverter {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] generateConversionCommand(String executable, File file, List<StreamObject> streams, String outFile) {
|
||||
List<String> command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, file.getName());
|
||||
@Nullable
|
||||
public FFMpegCommand generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
|
||||
@NotNull String outFile) {
|
||||
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
|
||||
List<ConverterModule> modules = new ArrayList<>();
|
||||
|
||||
List<StreamObject> streams = probeResult.parsedStreams();
|
||||
if (this.debug) {
|
||||
FFMpegHelper.addDebugArguments(command, 50, 120);
|
||||
modules.add(new DebugModule());
|
||||
}
|
||||
|
||||
//Add all streams without re-encoding
|
||||
command.add("-map");
|
||||
command.add("0");
|
||||
command.add("-c");
|
||||
command.add("copy");
|
||||
modules.add(new MapAllModule<>(streams));
|
||||
modules.add(new HardwareDecodeModule());
|
||||
modules.add(new CopyAllModule());
|
||||
|
||||
command.add(outFile);
|
||||
return command.toArray(new String[0]);
|
||||
modules.add(new SetOutputFileModule(outFile));
|
||||
|
||||
new ModuleExecutor(command, modules).execute();
|
||||
return command;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getValidFormats() {
|
||||
@NotNull
|
||||
public List<String> getValidFormats() {
|
||||
return videoFormats;
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,124 @@
|
||||
package net.knarcraft.ffmpegconverter.converter;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.FFMpegConvert;
|
||||
import net.knarcraft.ffmpegconverter.config.Configuration;
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.DebugModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ModuleExecutor;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.hardwarecoding.HardwareDecodeModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.mapping.AddStereoAudioStreamModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.mapping.BurnSubtitleModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.mapping.SelectSingleStreamModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.sorter.AudioLanguageSorter;
|
||||
import net.knarcraft.ffmpegconverter.converter.sorter.MinimalSubtitleSorter;
|
||||
import net.knarcraft.ffmpegconverter.converter.sorter.StreamSorter;
|
||||
import net.knarcraft.ffmpegconverter.converter.sorter.SubtitleLanguageSorter;
|
||||
import net.knarcraft.ffmpegconverter.converter.sorter.SubtitleTitleSorter;
|
||||
import net.knarcraft.ffmpegconverter.property.MinimalSubtitlePreference;
|
||||
import net.knarcraft.ffmpegconverter.streams.AudioStream;
|
||||
import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
|
||||
import net.knarcraft.ffmpegconverter.streams.VideoStream;
|
||||
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static net.knarcraft.ffmpegconverter.utility.FFMpegHelper.getNthSteam;
|
||||
|
||||
/**
|
||||
* A converter mainly designed for converting anime to web-playable mp4
|
||||
*/
|
||||
public class WebAnimeConverter extends AbstractConverter {
|
||||
|
||||
private final List<String> audioLanguages;
|
||||
private final List<String> subtitleLanguages;
|
||||
private final boolean toStereo;
|
||||
private final MinimalSubtitlePreference subtitlePreference;
|
||||
private final int forcedAudioIndex;
|
||||
private final int forcedSubtitleIndex;
|
||||
private final String subtitleNameFilter;
|
||||
|
||||
/**
|
||||
* Instantiates a new anime converter
|
||||
*
|
||||
* @param ffprobePath <p>Path/command to ffprobe.</p>
|
||||
* @param ffmpegPath <p>Path/command to ffmpeg.</p>
|
||||
* @param toStereo <p>Convert video with several audio channels to stereo.</p>
|
||||
* @param subtitlePreference <p>How minimal subtitles should be prioritized</p>
|
||||
* @param forcedAudioIndex <p>A specific audio stream to force. 0-indexed from the first audio stream found</p>
|
||||
* @param forcedSubtitleIndex <p>A specific subtitle stream to force. 0-indexed for the first subtitle stream found</p>
|
||||
*/
|
||||
public WebAnimeConverter(@NotNull String ffprobePath, @NotNull String ffmpegPath, boolean toStereo,
|
||||
@NotNull MinimalSubtitlePreference subtitlePreference, int forcedAudioIndex,
|
||||
int forcedSubtitleIndex, @NotNull String subtitleNameFilter) {
|
||||
super("mp4");
|
||||
Configuration configuration = FFMpegConvert.getConfiguration();
|
||||
this.ffprobePath = ffprobePath;
|
||||
this.ffmpegPath = ffmpegPath;
|
||||
this.audioLanguages = configuration.getAnimeAudioLanguages();
|
||||
this.subtitleLanguages = configuration.getAnimeSubtitleLanguages();
|
||||
this.toStereo = toStereo;
|
||||
this.subtitlePreference = subtitlePreference;
|
||||
this.forcedAudioIndex = forcedAudioIndex;
|
||||
this.forcedSubtitleIndex = forcedSubtitleIndex;
|
||||
this.subtitleNameFilter = subtitleNameFilter;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public FFMpegCommand generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
|
||||
@NotNull String outFile) {
|
||||
FFMpegCommand command = FFMpegHelper.getFFMpegWebVideoCommand(executable, probeResult.parsedFiles());
|
||||
List<ConverterModule> modules = new ArrayList<>();
|
||||
if (this.debug) {
|
||||
modules.add(new DebugModule());
|
||||
}
|
||||
|
||||
//Get the first audio stream in accordance with chosen languages
|
||||
AudioStream audioStream = getNthSteam(new AudioLanguageSorter(this.audioLanguages).sort(
|
||||
probeResult.getAudioStreams()), this.forcedAudioIndex);
|
||||
if (audioStream == null) {
|
||||
throw new IllegalArgumentException("The given input resulted in no audio stream being selected");
|
||||
}
|
||||
if (this.toStereo) {
|
||||
modules.add(new AddStereoAudioStreamModule(audioStream, true));
|
||||
} else {
|
||||
modules.add(new SelectSingleStreamModule(audioStream));
|
||||
}
|
||||
|
||||
//Get the first video stream
|
||||
VideoStream videoStream = getNthSteam(probeResult.getVideoStreams(), 0);
|
||||
|
||||
//Get the first subtitle stream in accordance with chosen languages and signs and songs prevention
|
||||
StreamSorter<SubtitleStream> subtitleSorter = new SubtitleTitleSorter(List.of(this.subtitleNameFilter))
|
||||
.append(new MinimalSubtitleSorter(this.subtitlePreference))
|
||||
.append(new SubtitleLanguageSorter(this.subtitleLanguages));
|
||||
SubtitleStream subtitleStream = getNthSteam(subtitleSorter.chainSort(probeResult.getSubtitleStreams()),
|
||||
this.forcedSubtitleIndex);
|
||||
|
||||
modules.add(new HardwareDecodeModule());
|
||||
if (subtitleStream != null && videoStream != null) {
|
||||
modules.add(new BurnSubtitleModule(subtitleStream, videoStream, true));
|
||||
} else if (videoStream != null) {
|
||||
modules.add(new SelectSingleStreamModule(videoStream));
|
||||
} else {
|
||||
throw new IllegalArgumentException("The selected video stream does not exist!");
|
||||
}
|
||||
modules.add(new SetOutputFileModule(outFile));
|
||||
|
||||
new ModuleExecutor(command, modules).execute();
|
||||
return command;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public List<String> getValidFormats() {
|
||||
return this.videoFormats;
|
||||
}
|
||||
|
||||
}
|
@ -1,14 +1,29 @@
|
||||
package net.knarcraft.ffmpegconverter.converter;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.streams.AudioStream;
|
||||
import net.knarcraft.ffmpegconverter.streams.StreamObject;
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.DebugModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ModuleExecutor;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.hardwarecoding.HardwareDecodeModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.mapping.BurnSubtitleModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.mapping.NthAudioStreamModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.mapping.SelectSingleStreamModule;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
|
||||
import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
|
||||
import net.knarcraft.ffmpegconverter.streams.VideoStream;
|
||||
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static net.knarcraft.ffmpegconverter.utility.FFMpegHelper.getNthSteam;
|
||||
|
||||
/**
|
||||
* A simple converter for web-video
|
||||
*/
|
||||
public class WebVideoConverter extends AbstractConverter {
|
||||
|
||||
/**
|
||||
@ -25,29 +40,41 @@ public class WebVideoConverter extends AbstractConverter {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getValidFormats() {
|
||||
@NotNull
|
||||
public List<String> getValidFormats() {
|
||||
return videoFormats;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] generateConversionCommand(String executable, File file, List<StreamObject> streams, String outFile) {
|
||||
List<String> command = FFMpegHelper.getFFMpegWebVideoCommand(executable, file.getName());
|
||||
@Nullable
|
||||
public FFMpegCommand generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
|
||||
@NotNull String outFile) {
|
||||
FFMpegCommand command = FFMpegHelper.getFFMpegWebVideoCommand(executable, probeResult.parsedFiles());
|
||||
List<ConverterModule> modules = new ArrayList<>();
|
||||
|
||||
if (this.debug) {
|
||||
FFMpegHelper.addDebugArguments(command, 50, 120);
|
||||
modules.add(new DebugModule());
|
||||
}
|
||||
|
||||
//Get first streams from the file
|
||||
SubtitleStream subtitleStream = getFirstSubtitleStream(streams);
|
||||
VideoStream videoStream = getFirstVideoStream(streams);
|
||||
AudioStream audioStream = getFirstAudioSteam(streams);
|
||||
SubtitleStream subtitleStream = getNthSteam(probeResult.getSubtitleStreams(), 0);
|
||||
VideoStream videoStream = getNthSteam(probeResult.getVideoStreams(), 0);
|
||||
|
||||
//Add streams to output
|
||||
FFMpegHelper.addSubtitleAndVideoStream(command, subtitleStream, videoStream, file);
|
||||
if (audioStream != null) {
|
||||
FFMpegHelper.addAudioStream(command, audioStream, true);
|
||||
if (videoStream == null) {
|
||||
throw new IllegalArgumentException("The selected video stream does not exist.");
|
||||
}
|
||||
|
||||
command.add(outFile);
|
||||
return command.toArray(new String[0]);
|
||||
if (subtitleStream != null) {
|
||||
modules.add(new BurnSubtitleModule(subtitleStream, videoStream, true));
|
||||
} else {
|
||||
modules.add(new SelectSingleStreamModule(videoStream));
|
||||
}
|
||||
modules.add(new NthAudioStreamModule(probeResult.getAudioStreams(), 0));
|
||||
modules.add(new HardwareDecodeModule());
|
||||
modules.add(new SetOutputFileModule(outFile));
|
||||
|
||||
new ModuleExecutor(command, modules).execute();
|
||||
return command;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,18 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.module;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A module that adds some arguments to a ffmpeg command
|
||||
*/
|
||||
public interface ConverterModule {
|
||||
|
||||
/**
|
||||
* Adds this module's arguments to the given command
|
||||
*
|
||||
* @param command <p>The command to add to</p>
|
||||
*/
|
||||
void addArguments(@NotNull FFMpegCommand command);
|
||||
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.module;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A converter module for adding de-interlacing
|
||||
*/
|
||||
public class DeInterlaceModule implements ConverterModule {
|
||||
|
||||
@Override
|
||||
public void addArguments(@NotNull FFMpegCommand command) {
|
||||
command.addOutputFileOption("-filter:v", "bwdif=mode=send_field");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.module;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A module for adding options useful for debugging
|
||||
*/
|
||||
public class DebugModule implements ConverterModule {
|
||||
|
||||
private double startTime = 50;
|
||||
private double duration = 120;
|
||||
|
||||
/**
|
||||
* Instantiates a new debug module that starts at 50 seconds, and lasts until 120 seconds
|
||||
*/
|
||||
public DebugModule() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new debug module
|
||||
*
|
||||
* @param startTime <p>The time to start at</p>
|
||||
* @param duration <p>The time to stop at</p>
|
||||
*/
|
||||
public DebugModule(double startTime, double duration) {
|
||||
if (startTime > 0) {
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
if (duration > 0) {
|
||||
this.duration = duration;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addArguments(@NotNull FFMpegCommand command) {
|
||||
command.addInputFileOption("-ss", String.valueOf(this.startTime));
|
||||
command.addOutputFileOption("-t", String.valueOf(this.duration));
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.module;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An executor for executing a list of modules
|
||||
*/
|
||||
public class ModuleExecutor {
|
||||
|
||||
private final FFMpegCommand command;
|
||||
private final List<ConverterModule> modules;
|
||||
|
||||
/**
|
||||
* Instantiates a new module executor
|
||||
*
|
||||
* @param command <p>The command to alter</p>
|
||||
* @param modules <p>The models to execute</p>
|
||||
*/
|
||||
public ModuleExecutor(@NotNull FFMpegCommand command, @NotNull List<ConverterModule> modules) {
|
||||
this.command = command;
|
||||
this.modules = modules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds arguments for all the specified modules to the FFMpeg command
|
||||
*/
|
||||
public void execute() {
|
||||
for (ConverterModule module : this.modules) {
|
||||
module.addArguments(this.command);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.module.hardwarecoding;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A module for setting output as h264, accelerated with Nvidia hardware
|
||||
*/
|
||||
public class H264HardwareEncodingModule implements ConverterModule {
|
||||
|
||||
private final int quality;
|
||||
|
||||
/**
|
||||
* Instantiates a new h264 hardware encoding module
|
||||
*
|
||||
* @param quality <p>The crf quality to use</p>
|
||||
*/
|
||||
public H264HardwareEncodingModule(int quality) {
|
||||
this.quality = quality;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addArguments(@NotNull FFMpegCommand command) {
|
||||
FFMpegHelper.addH264HardwareEncoding(command, this.quality);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.module.hardwarecoding;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A module for setting output as h265 (hevc), accelerated with Nvidia hardware
|
||||
*/
|
||||
public class H265HardwareEncodingModule implements ConverterModule {
|
||||
|
||||
private final int quality;
|
||||
|
||||
/**
|
||||
* Instantiates a new h265 (hevc) hardware encoding module
|
||||
*
|
||||
* @param quality <p>The crf quality to use</p>
|
||||
*/
|
||||
public H265HardwareEncodingModule(int quality) {
|
||||
this.quality = quality;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addArguments(@NotNull FFMpegCommand command) {
|
||||
FFMpegHelper.addH265HardwareEncoding(command, this.quality);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.module.hardwarecoding;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A module for enabling hardware decoding
|
||||
*/
|
||||
public class HardwareDecodeModule implements ConverterModule {
|
||||
|
||||
@Override
|
||||
public void addArguments(@NotNull FFMpegCommand command) {
|
||||
command.addInputFileOption("-hwaccel", "auto");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.module.mapping;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import net.knarcraft.ffmpegconverter.streams.AudioStream;
|
||||
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A module for adding an audio stream, and converting it to stereo
|
||||
*/
|
||||
public class AddStereoAudioStreamModule implements ConverterModule {
|
||||
|
||||
private final AudioStream audioStream;
|
||||
private final boolean mapAudio;
|
||||
|
||||
/**
|
||||
* Instantiates a new add stereo audio stream module
|
||||
*
|
||||
* @param audioStream <p>The audio stream to add and convert to stereo</p>
|
||||
* @param mapAudio <p>Whether to map the given audio stream (only disable if mapped elsewhere)</p>
|
||||
*/
|
||||
public AddStereoAudioStreamModule(@NotNull AudioStream audioStream, boolean mapAudio) {
|
||||
this.audioStream = audioStream;
|
||||
this.mapAudio = mapAudio;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addArguments(@NotNull FFMpegCommand command) {
|
||||
if (mapAudio) {
|
||||
FFMpegHelper.mapStream(command, audioStream);
|
||||
}
|
||||
if (audioStream.getChannels() > 2) {
|
||||
command.addOutputFileOption("-af", "pan=stereo|FL=FC+0.30*FL+0.30*BL|FR=FC+0.30*FR+0.30*BR");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.module.mapping;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
|
||||
import net.knarcraft.ffmpegconverter.streams.VideoStream;
|
||||
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A module for burning the selected subtitle into a video
|
||||
*/
|
||||
public class BurnSubtitleModule implements ConverterModule {
|
||||
|
||||
private final SubtitleStream subtitleStream;
|
||||
private final VideoStream videoStream;
|
||||
private final boolean mapVideo;
|
||||
|
||||
/**
|
||||
* Instantiates a subtitle burning converter
|
||||
*
|
||||
* @param subtitleStream <p>The subtitle stream to burn to a video stream</p>
|
||||
* @param videoStream <p>The video stream to burn into</p>
|
||||
* @param mapVideo <p>Whether to map the given video stream (only disable if mapped elsewhere)</p>
|
||||
*/
|
||||
public BurnSubtitleModule(@NotNull SubtitleStream subtitleStream, @NotNull VideoStream videoStream,
|
||||
boolean mapVideo) {
|
||||
this.subtitleStream = subtitleStream;
|
||||
this.videoStream = videoStream;
|
||||
this.mapVideo = mapVideo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addArguments(@NotNull FFMpegCommand command) {
|
||||
if (mapVideo) {
|
||||
FFMpegHelper.mapStream(command, videoStream);
|
||||
}
|
||||
|
||||
if (subtitleStream.isImageSubtitle()) {
|
||||
command.addOutputFileOption("-filter_complex",
|
||||
String.format("[%d:%d]scale=width=%d:height=%d,crop=w=%d:h=%d:x=0:y=out_h[sub];[%d:%d][sub]overlay",
|
||||
subtitleStream.getInputIndex(), subtitleStream.getAbsoluteIndex(), videoStream.getWidth(),
|
||||
videoStream.getHeight(), videoStream.getWidth(), videoStream.getHeight(),
|
||||
videoStream.getInputIndex(), videoStream.getAbsoluteIndex()));
|
||||
command.addOutputFileOption("-profile:v", "baseline");
|
||||
} else {
|
||||
String safeFileName = FFMpegHelper.escapeSpecialCharactersInFileName(
|
||||
command.getInputFiles().get(subtitleStream.getInputIndex()));
|
||||
String subtitleCommand = String.format("subtitles='%s':si=%d", safeFileName,
|
||||
subtitleStream.getRelativeIndex());
|
||||
command.addOutputFileOption("-vf", subtitleCommand);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.module.mapping;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import net.knarcraft.ffmpegconverter.streams.StreamObject;
|
||||
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A module for mapping all the given streams
|
||||
*/
|
||||
public class MapAllModule<K extends StreamObject> implements ConverterModule {
|
||||
|
||||
final List<K> streams;
|
||||
|
||||
/**
|
||||
* Instantiates a new map all module
|
||||
*
|
||||
* @param streams <p>The streams to map</p>
|
||||
*/
|
||||
public MapAllModule(@NotNull List<K> streams) {
|
||||
this.streams = streams;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addArguments(@NotNull FFMpegCommand command) {
|
||||
FFMpegHelper.mapAllStreams(command, this.streams);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.module.mapping;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import net.knarcraft.ffmpegconverter.streams.AudioStream;
|
||||
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A module for selecting and mapping the nth audio stream available
|
||||
*/
|
||||
public class NthAudioStreamModule implements ConverterModule {
|
||||
|
||||
private final int n;
|
||||
private final List<AudioStream> allStreams;
|
||||
|
||||
/**
|
||||
* Instantiates a new n-th audio stream module
|
||||
*
|
||||
* @param allStreams <p>All available streams</p>
|
||||
* @param n <p>The index of the stream the user wants</p>
|
||||
*/
|
||||
public NthAudioStreamModule(@NotNull List<AudioStream> allStreams, int n) {
|
||||
this.allStreams = allStreams;
|
||||
this.n = n;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addArguments(@NotNull FFMpegCommand command) {
|
||||
AudioStream audioStream = FFMpegHelper.getNthSteam(this.allStreams, this.n);
|
||||
if (audioStream != null) {
|
||||
FFMpegHelper.mapStream(command, audioStream);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Selected audio stream does not exist.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.module.mapping;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
|
||||
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A module for selecting and mapping the nth subtitle stream available
|
||||
*/
|
||||
public class NthSubtitleStreamModule implements ConverterModule {
|
||||
private final int n;
|
||||
private final List<SubtitleStream> allStreams;
|
||||
|
||||
/**
|
||||
* Instantiates a new n-th video stream module
|
||||
*
|
||||
* @param allStreams <p>All available streams</p>
|
||||
* @param n <p>The index of the stream the user wants</p>
|
||||
*/
|
||||
public NthSubtitleStreamModule(@NotNull List<SubtitleStream> allStreams, int n) {
|
||||
this.allStreams = allStreams;
|
||||
this.n = n;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addArguments(@NotNull FFMpegCommand command) {
|
||||
SubtitleStream subtitleStream = FFMpegHelper.getNthSteam(this.allStreams, this.n);
|
||||
if (subtitleStream != null) {
|
||||
FFMpegHelper.mapStream(command, subtitleStream);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Selected subtitle stream does not exist.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.module.mapping;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import net.knarcraft.ffmpegconverter.streams.VideoStream;
|
||||
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class NthVideoStreamModule implements ConverterModule {
|
||||
|
||||
private final int n;
|
||||
private final List<VideoStream> allStreams;
|
||||
|
||||
/**
|
||||
* Instantiates a new n-th video stream module
|
||||
*
|
||||
* @param allStreams <p>All available streams</p>
|
||||
* @param n <p>The index of the stream the user wants</p>
|
||||
*/
|
||||
public NthVideoStreamModule(@NotNull List<VideoStream> allStreams, int n) {
|
||||
this.allStreams = allStreams;
|
||||
this.n = n;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addArguments(@NotNull FFMpegCommand command) {
|
||||
VideoStream videoStream = FFMpegHelper.getNthSteam(this.allStreams, this.n);
|
||||
if (videoStream != null) {
|
||||
FFMpegHelper.mapStream(command, videoStream);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Selected video stream does not exist.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.module.mapping;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import net.knarcraft.ffmpegconverter.streams.StreamObject;
|
||||
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A module for selecting and mapping a single stream
|
||||
*/
|
||||
public class SelectSingleStreamModule implements ConverterModule {
|
||||
|
||||
private final StreamObject stream;
|
||||
|
||||
/**
|
||||
* Instantiates a new select single stream module
|
||||
*
|
||||
* @param stream <p>The stream to map to the output file</p>
|
||||
*/
|
||||
public SelectSingleStreamModule(@NotNull StreamObject stream) {
|
||||
this.stream = stream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addArguments(@NotNull FFMpegCommand command) {
|
||||
FFMpegHelper.mapStream(command, stream);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.module.mapping;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import net.knarcraft.ffmpegconverter.streams.StreamObject;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A module for selecting one or more audio streams, sorted by language
|
||||
*/
|
||||
public class SetDefaultStreamModule<K extends StreamObject> implements ConverterModule {
|
||||
|
||||
private final List<K> streams;
|
||||
private final int defaultStream;
|
||||
|
||||
/**
|
||||
* Instantiates a new language sorted audio stream module
|
||||
*
|
||||
* @param streams <p>All input streams</p>
|
||||
* @param defaultStream <p>The index of the output stream to set as default</p>
|
||||
*/
|
||||
public SetDefaultStreamModule(@NotNull List<K> streams, int defaultStream) {
|
||||
this.streams = streams;
|
||||
this.defaultStream = defaultStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addArguments(@NotNull FFMpegCommand command) {
|
||||
for (int i = 0; i < streams.size(); i++) {
|
||||
K stream = streams.get(i);
|
||||
char defaultModifier;
|
||||
if (i == defaultStream) {
|
||||
defaultModifier = '+';
|
||||
} else {
|
||||
defaultModifier = '-';
|
||||
}
|
||||
command.addOutputFileOption(String.format("-disposition:%s:%d", stream.streamTypeCharacter(), i),
|
||||
String.format("%sdefault-forced", defaultModifier));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.module.output;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A module for making FFMpeg copy all codecs
|
||||
*/
|
||||
public class CopyAllModule implements ConverterModule {
|
||||
|
||||
@Override
|
||||
public void addArguments(@NotNull FFMpegCommand command) {
|
||||
command.addOutputFileOption("-c", "copy");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.module.output;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import net.knarcraft.ffmpegconverter.streams.AudioStream;
|
||||
import net.knarcraft.ffmpegconverter.streams.StreamObject;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A module for making FFMpeg copy the audio codec
|
||||
*/
|
||||
public class CopyAudioModule implements ConverterModule {
|
||||
|
||||
private final List<AudioStream> streams;
|
||||
|
||||
/**
|
||||
* Instantiates a new copy audio module
|
||||
*
|
||||
* @param streams <p>The streams to specify the copy flag for, or null to not use a per-stream selector</p>
|
||||
*/
|
||||
public CopyAudioModule(@NotNull List<AudioStream> streams) {
|
||||
this.streams = streams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new copy audio module
|
||||
*
|
||||
* @param stream <p>The stream to specify the copy flag for</p>
|
||||
*/
|
||||
public CopyAudioModule(@NotNull AudioStream stream) {
|
||||
this.streams = List.of(stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new copy audio module
|
||||
*/
|
||||
public CopyAudioModule() {
|
||||
this.streams = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addArguments(@NotNull FFMpegCommand command) {
|
||||
if (this.streams != null) {
|
||||
for (StreamObject streamObject : this.streams) {
|
||||
int outputIndex = streamObject.getOutputIndex();
|
||||
if (outputIndex != -1) {
|
||||
command.addOutputFileOption("-c:a:" + outputIndex, "copy");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
command.addOutputFileOption("-c:a", "copy");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.module.output;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A module for making FFMpeg copy the subtitle codec
|
||||
*/
|
||||
public class CopySubtitlesModule implements ConverterModule {
|
||||
|
||||
@Override
|
||||
public void addArguments(@NotNull FFMpegCommand command) {
|
||||
command.addOutputFileOption("-c:s", "copy");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.module.output;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A module for making FFMpeg copy the video codec
|
||||
*/
|
||||
public class CopyVideoModule implements ConverterModule {
|
||||
|
||||
@Override
|
||||
public void addArguments(@NotNull FFMpegCommand command) {
|
||||
command.addOutputFileOption("-c:v", "copy");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.module.output;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A module for adding the fast start flag (immediate playback)
|
||||
*/
|
||||
public class FastStartModule implements ConverterModule {
|
||||
|
||||
@Override
|
||||
public void addArguments(@NotNull FFMpegCommand command) {
|
||||
command.addOutputFileOption("-movflags", "+faststart");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.module.output;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A module for setting subtitle codec to mov_text (necessary for embedding in .mp4 files)
|
||||
*/
|
||||
public class MovTextModule implements ConverterModule {
|
||||
|
||||
@Override
|
||||
public void addArguments(@NotNull FFMpegCommand command) {
|
||||
command.addOutputFileOption("-c:s", "mov_text");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.module.output;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A module for scaling the resolution of video streams
|
||||
*/
|
||||
public class ScaleModule implements ConverterModule {
|
||||
|
||||
final int newWidth;
|
||||
final int newHeight;
|
||||
|
||||
/**
|
||||
* Instantiates a new scale module
|
||||
*
|
||||
* @param newWidth <p>The new width of the video stream</p>
|
||||
* @param newHeight <p>The new height of the video stream</p>
|
||||
*/
|
||||
public ScaleModule(int newWidth, int newHeight) {
|
||||
this.newWidth = newWidth;
|
||||
this.newHeight = newHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addArguments(@NotNull FFMpegCommand command) {
|
||||
command.addOutputFileOption("-vf", "scale=" + this.newWidth + ":" + this.newHeight);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.module.output;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import net.knarcraft.ffmpegconverter.utility.FileUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A module for setting the output file
|
||||
*/
|
||||
public class SetOutputFileModule implements ConverterModule {
|
||||
|
||||
private final String outputFile;
|
||||
|
||||
/**
|
||||
* Instantiates a new set output file module
|
||||
*
|
||||
* @param outputFile <p>The output file to set</p>
|
||||
*/
|
||||
public SetOutputFileModule(@NotNull String outputFile) {
|
||||
this.outputFile = outputFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addArguments(@NotNull FFMpegCommand command) {
|
||||
command.setOutputFile(this.outputFile);
|
||||
if (FileUtil.getExtension(this.outputFile).equals("mkv")) {
|
||||
command.addOutputFileOption("-f", "matroska");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.module.output;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A module for setting output quality
|
||||
*/
|
||||
public class SetQualityModule implements ConverterModule {
|
||||
|
||||
private final int crf;
|
||||
private final String preset;
|
||||
|
||||
/**
|
||||
* Instantiates a new quality module
|
||||
*
|
||||
* @param crf <p>The CRF to set. 0 = lossless, 51 = terrible, 17 is visually lossless</p>
|
||||
* @param preset <p>The preset to use (p1-p7, p7 is slowest and best)</p>
|
||||
*/
|
||||
public SetQualityModule(int crf, @NotNull String preset) {
|
||||
this.crf = crf;
|
||||
this.preset = preset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addArguments(@NotNull FFMpegCommand command) {
|
||||
command.addOutputFileOption("-crf", String.valueOf(crf));
|
||||
command.addOutputFileOption("-preset", this.preset);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.module.output;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import net.knarcraft.ffmpegconverter.streams.StreamObject;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A module for setting the language of some streams
|
||||
*
|
||||
* @param <K> <p>The type of stream to set language for</p>
|
||||
*/
|
||||
public class SetStreamLanguageModule<K extends StreamObject> implements ConverterModule {
|
||||
|
||||
private final List<K> streams;
|
||||
|
||||
/**
|
||||
* Instantiates a new set stream language module
|
||||
*
|
||||
* @param streams <p>The streams to set language for</p>
|
||||
*/
|
||||
public SetStreamLanguageModule(@NotNull List<K> streams) {
|
||||
this.streams = streams;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addArguments(@NotNull FFMpegCommand command) {
|
||||
for (StreamObject stream : this.streams) {
|
||||
if (!stream.getLanguage().equalsIgnoreCase("und") && !stream.getLanguage().isBlank()) {
|
||||
command.addOutputFileOption(String.format("-metadata:s:%s:%d", stream.streamTypeCharacter(),
|
||||
stream.getOutputIndex()), String.format("language=%s", stream.getLanguage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.module.output;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A module for setting the video codec
|
||||
*/
|
||||
public class SetVideoCodecModule implements ConverterModule {
|
||||
|
||||
private final String codec;
|
||||
|
||||
/**
|
||||
* Instantiates a new set video codec module
|
||||
*
|
||||
* @param codec <p>The codec to set</p>
|
||||
*/
|
||||
public SetVideoCodecModule(@NotNull String codec) {
|
||||
this.codec = codec;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addArguments(@NotNull FFMpegCommand command) {
|
||||
command.addOutputFileOption("-vcodec", codec);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.sorter;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.streams.StreamObject;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An abstract stream sorter, making implementation easier
|
||||
*
|
||||
* @param <L> <p>The type of streams this sorter sorts</p>
|
||||
*/
|
||||
public abstract class AbstractSorter<L extends StreamObject> implements StreamSorter<L> {
|
||||
|
||||
protected StreamSorter<L> nextSorter = null;
|
||||
|
||||
@Override
|
||||
public @NotNull StreamSorter<L> prepend(@NotNull StreamSorter<L> other) {
|
||||
this.nextSorter = other;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull StreamSorter<L> append(@NotNull StreamSorter<L> other) {
|
||||
StreamSorter<L> end = other;
|
||||
while (end != null && end.hasChainElement()) {
|
||||
end = end.getNextInChain();
|
||||
}
|
||||
if (end == null) {
|
||||
throw new IllegalStateException("Other cannot be null. Something is wrong!");
|
||||
}
|
||||
end.setNextInChain(this);
|
||||
return other;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<L> chainSort(@NotNull List<L> input) {
|
||||
List<L> sorted = this.sort(input);
|
||||
if (nextSorter != null) {
|
||||
return nextSorter.chainSort(sorted);
|
||||
} else {
|
||||
return sorted;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasChainElement() {
|
||||
return this.nextSorter != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public StreamSorter<L> getNextInChain() {
|
||||
return this.nextSorter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNextInChain(@NotNull StreamSorter<L> next) {
|
||||
this.nextSorter = next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts subtitle streams according to chosen languages and removes non-matching languages
|
||||
*
|
||||
* @param streams <p>A list of streams to sort.</p>
|
||||
* @param languages <p>The languages chosen by the user.</p>
|
||||
* @param <G> <p>The type of streams to sort.</p>
|
||||
* @return <p>A sorted version of the list.</p>
|
||||
*/
|
||||
@NotNull
|
||||
protected <G extends StreamObject> List<G> sortStreamsByLanguage(@NotNull List<G> streams,
|
||||
@NotNull List<String> languages) {
|
||||
List<G> sorted = new ArrayList<>();
|
||||
for (String language : languages) {
|
||||
for (G stream : streams) {
|
||||
String streamLanguage = stream.getLanguage();
|
||||
if (language.equals("*") || (streamLanguage.equals("und") && language.equals("0")) ||
|
||||
streamLanguage.equals(language)) {
|
||||
sorted.add(stream);
|
||||
}
|
||||
}
|
||||
streams.removeAll(sorted);
|
||||
}
|
||||
return sorted;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.sorter;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.streams.AudioStream;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A sorter for sorting audio streams by language
|
||||
*/
|
||||
public class AudioLanguageSorter extends AbstractSorter<AudioStream> {
|
||||
|
||||
private final List<String> languageOrder;
|
||||
|
||||
/**
|
||||
* Instantiates a new audio language sorter
|
||||
*
|
||||
* @param languageOrder <p>The order of preference for audio languages</p>
|
||||
*/
|
||||
public AudioLanguageSorter(@NotNull List<String> languageOrder) {
|
||||
this.languageOrder = languageOrder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<AudioStream> sort(@NotNull List<AudioStream> input) {
|
||||
return sortStreamsByLanguage(input, this.languageOrder);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.sorter;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.streams.StreamObject;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A sorter that puts the stream with the given relative index first, without changing order of other items
|
||||
*
|
||||
* @param <K> <p>The type of stream to sort</p>
|
||||
*/
|
||||
public class ForcedFirstSorter<K extends StreamObject> extends AbstractSorter<K> {
|
||||
|
||||
private final int forcedIndex;
|
||||
|
||||
/**
|
||||
* Instantiates a new forced first sorter
|
||||
*
|
||||
* @param forcedIndex <p>The relative index of the stream to set as the first item</p>
|
||||
*/
|
||||
public ForcedFirstSorter(int forcedIndex) {
|
||||
this.forcedIndex = forcedIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<K> sort(@NotNull List<K> input) {
|
||||
if (input.isEmpty()) {
|
||||
return input;
|
||||
}
|
||||
|
||||
int listIndex = 0;
|
||||
for (int i = 0; i < input.size(); i++) {
|
||||
if (input.get(i).getRelativeIndex() == forcedIndex) {
|
||||
listIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
List<K> output = new ArrayList<>(input);
|
||||
K first = output.remove(listIndex);
|
||||
output.add(0, first);
|
||||
return output;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.sorter;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.property.MinimalSubtitlePreference;
|
||||
import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A sorter for sorting/filtering subtitles by a minimal subtitle preference
|
||||
*/
|
||||
public class MinimalSubtitleSorter extends AbstractSorter<SubtitleStream> {
|
||||
|
||||
private final MinimalSubtitlePreference minimalSubtitlePreference;
|
||||
|
||||
/**
|
||||
* Instantiates a new minimal subtitle preference sorter
|
||||
*
|
||||
* @param minimalSubtitlePreference <p>The minimal subtitle preference sort/filter by</p>
|
||||
*/
|
||||
public MinimalSubtitleSorter(@NotNull MinimalSubtitlePreference minimalSubtitlePreference) {
|
||||
this.minimalSubtitlePreference = minimalSubtitlePreference;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<SubtitleStream> sort(@NotNull List<SubtitleStream> input) {
|
||||
// Split all subtitles into full and minimal
|
||||
List<SubtitleStream> fullSubtitles = new ArrayList<>();
|
||||
List<SubtitleStream> minimalSubtitles = new ArrayList<>();
|
||||
for (SubtitleStream subtitleStream : input) {
|
||||
if (subtitleStream.isFullSubtitle()) {
|
||||
fullSubtitles.add(subtitleStream);
|
||||
} else {
|
||||
minimalSubtitles.add(subtitleStream);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort/filter subtitles based on full and minimal
|
||||
switch (this.minimalSubtitlePreference) {
|
||||
case REJECT -> {
|
||||
// Only return full subtitles
|
||||
return fullSubtitles;
|
||||
}
|
||||
case REQUIRE -> {
|
||||
// Only return minimal subtitles
|
||||
return minimalSubtitles;
|
||||
}
|
||||
case NO_PREFERENCE -> {
|
||||
// Don't change order
|
||||
return input;
|
||||
}
|
||||
case PREFER -> {
|
||||
// Sort minimal subtitles first, and full subtitles last
|
||||
minimalSubtitles.addAll(fullSubtitles);
|
||||
return minimalSubtitles;
|
||||
}
|
||||
case AVOID -> {
|
||||
// Sort full subtitles first, and minimal subtitles last
|
||||
fullSubtitles.addAll(minimalSubtitles);
|
||||
return fullSubtitles;
|
||||
}
|
||||
default -> throw new IllegalStateException("Unknown enum value encountered");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.sorter;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.property.MinimalSubtitlePreference;
|
||||
import net.knarcraft.ffmpegconverter.streams.AudioStream;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A sorter for sorting/filtering subtitles by a minimal subtitle preference
|
||||
*/
|
||||
public class SpecialAudioSorter extends AbstractSorter<AudioStream> {
|
||||
|
||||
private final MinimalSubtitlePreference minimalSubtitlePreference;
|
||||
|
||||
/**
|
||||
* Instantiates a new special audio preference sorter
|
||||
*
|
||||
* @param minimalSubtitlePreference <p>The minimal subtitle preference sort/filter by</p>
|
||||
*/
|
||||
public SpecialAudioSorter(@NotNull MinimalSubtitlePreference minimalSubtitlePreference) {
|
||||
this.minimalSubtitlePreference = minimalSubtitlePreference;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<AudioStream> sort(@NotNull List<AudioStream> input) {
|
||||
// Split all subtitles into full and minimal
|
||||
List<AudioStream> normalAudio = new ArrayList<>();
|
||||
List<AudioStream> specialAudio = new ArrayList<>();
|
||||
for (AudioStream audioStream : input) {
|
||||
if (audioStream.isSpecialAudio()) {
|
||||
specialAudio.add(audioStream);
|
||||
} else {
|
||||
normalAudio.add(audioStream);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort/filter subtitles based on full and minimal
|
||||
switch (this.minimalSubtitlePreference) {
|
||||
case REJECT -> {
|
||||
// Only return full subtitles
|
||||
return normalAudio;
|
||||
}
|
||||
case REQUIRE -> {
|
||||
// Only return minimal subtitles
|
||||
return specialAudio;
|
||||
}
|
||||
case NO_PREFERENCE -> {
|
||||
// Don't change order
|
||||
return input;
|
||||
}
|
||||
case PREFER -> {
|
||||
// Sort minimal subtitles first, and full subtitles last
|
||||
specialAudio.addAll(normalAudio);
|
||||
return specialAudio;
|
||||
}
|
||||
case AVOID -> {
|
||||
// Sort full subtitles first, and minimal subtitles last
|
||||
normalAudio.addAll(specialAudio);
|
||||
return normalAudio;
|
||||
}
|
||||
default -> throw new IllegalStateException("Unknown enum value encountered");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.sorter;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.streams.StreamObject;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An interface describing a chaining-capable stream sorter
|
||||
*
|
||||
* @param <K> <p>The type of stream this sorter sorts</p>
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public interface StreamSorter<K extends StreamObject> {
|
||||
|
||||
/**
|
||||
* Prepends this stream sorter to another
|
||||
*
|
||||
* <p>This stream sorter's next in chain will be set to other</p>
|
||||
*
|
||||
* @param other <p>The stream sorter to prepend</p>
|
||||
* @return <p>A reference to the first stream sorter in the current chain</p>
|
||||
*/
|
||||
@NotNull
|
||||
StreamSorter<K> prepend(@NotNull StreamSorter<K> other);
|
||||
|
||||
/**
|
||||
* Appends this stream sorter to another
|
||||
*
|
||||
* <p>This stream sorter is added to the end of other's chain</p>
|
||||
*
|
||||
* @param other <p>The stream sorter to append to this one</p>
|
||||
* @return <p>A reference to the first stream sorter in the current chain</p>
|
||||
*/
|
||||
@NotNull
|
||||
StreamSorter<K> append(@NotNull StreamSorter<K> other);
|
||||
|
||||
/**
|
||||
* Sorts the given input streams using this sorter only
|
||||
*
|
||||
* @param input <p>The input to sort</p>
|
||||
* @return <p>The sorted input</p>
|
||||
*/
|
||||
@NotNull
|
||||
List<K> sort(@NotNull List<K> input);
|
||||
|
||||
/**
|
||||
* Sorts the given input streams using all sorters in the chain
|
||||
*
|
||||
* @param input <p>The input to sort</p>
|
||||
* @return <p>The sorted input</p>
|
||||
*/
|
||||
@NotNull
|
||||
List<K> chainSort(@NotNull List<K> input);
|
||||
|
||||
/**
|
||||
* Gets whether this stream sorter has a sorter set as the next in its chain
|
||||
*
|
||||
* @return <p>True if a next chain item exists</p>
|
||||
*/
|
||||
boolean hasChainElement();
|
||||
|
||||
/**
|
||||
* Gets the next item in this stream sorter's chain
|
||||
*
|
||||
* @return <p>The next item in the chain</p>
|
||||
*/
|
||||
@Nullable
|
||||
StreamSorter<K> getNextInChain();
|
||||
|
||||
/**
|
||||
* Sets the given stream sorter as the next in the sorter chain
|
||||
*
|
||||
* @param next <p>The next item in the sorter chain</p>
|
||||
*/
|
||||
void setNextInChain(@NotNull StreamSorter<K> next);
|
||||
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.sorter;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A sorter for sorting subtitles by language
|
||||
*/
|
||||
public class SubtitleLanguageSorter extends AbstractSorter<SubtitleStream> {
|
||||
|
||||
private final List<String> languageOrder;
|
||||
|
||||
/**
|
||||
* Instantiates a new subtitle language sorter
|
||||
*
|
||||
* @param languageOrder <p>The order of preference for subtitle languages</p>
|
||||
*/
|
||||
public SubtitleLanguageSorter(@NotNull List<String> languageOrder) {
|
||||
this.languageOrder = languageOrder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<SubtitleStream> sort(@NotNull List<SubtitleStream> input) {
|
||||
return sortStreamsByLanguage(new ArrayList<>(input), this.languageOrder);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
package net.knarcraft.ffmpegconverter.converter.sorter;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
|
||||
import net.knarcraft.ffmpegconverter.utility.OutputUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
|
||||
/**
|
||||
* A sorter for filtering subtitle streams by title
|
||||
*/
|
||||
public class SubtitleTitleSorter extends AbstractSorter<SubtitleStream> {
|
||||
|
||||
private final List<String> titleFilter;
|
||||
|
||||
/**
|
||||
* Instantiates a new subtitle title sorter
|
||||
*
|
||||
* <p>If a simple string, or invalid RegEx is given, any stream containing the titleFilter in its title will be
|
||||
* retained. If a valid RegEx is given, any stream matching the titleFilter is retained.</p>
|
||||
*
|
||||
* @param titleFilter <p>The filter to use. RegEx match, or a string the title must contain.</p>
|
||||
*/
|
||||
public SubtitleTitleSorter(@NotNull List<String> titleFilter) {
|
||||
this.titleFilter = new ArrayList<>(titleFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<SubtitleStream> sort(@NotNull List<SubtitleStream> input) {
|
||||
// Don't change anything if no filter is given
|
||||
this.titleFilter.removeIf(String::isBlank);
|
||||
if (this.titleFilter.isEmpty()) {
|
||||
return input;
|
||||
}
|
||||
|
||||
List<SubtitleStream> output = new ArrayList<>();
|
||||
|
||||
for (String filter : this.titleFilter) {
|
||||
if (filter.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean isRegEx = isValidRegularExpression(filter) && hasSpecialRegexCharacters(filter);
|
||||
|
||||
OutputUtil.printDebug("Filtering subtitles by filter " + filter + ". RegEx is " + isRegEx);
|
||||
|
||||
for (SubtitleStream subtitleStream : input) {
|
||||
String title = subtitleStream.getTitle().trim().toLowerCase();
|
||||
// Add the subtitle if the filter matches, and it hasn't been added already
|
||||
boolean matches = filter.trim().equals("*") || (isRegEx && title.matches(filter.toLowerCase())) ||
|
||||
(!isRegEx && title.contains(filter.toLowerCase()));
|
||||
if (matches && !output.contains(subtitleStream)) {
|
||||
OutputUtil.printDebug("Subtitle stream with title " + title + " matches");
|
||||
output.add(subtitleStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given string is a valid regular expression
|
||||
*
|
||||
* @param input <p>The string to check</p>
|
||||
* @return <p>True if the given string has no invalid expressions</p>
|
||||
*/
|
||||
private static boolean isValidRegularExpression(@NotNull String input) {
|
||||
try {
|
||||
Pattern.compile(input);
|
||||
return true;
|
||||
} catch (PatternSyntaxException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the input string has any RegEx special characters
|
||||
*
|
||||
* @param input <p>The input to check</p>
|
||||
* @return <p>True if RegEx characters exist in the string</p>
|
||||
*/
|
||||
private static boolean hasSpecialRegexCharacters(String input) {
|
||||
Pattern regexSpecialCharacters = Pattern.compile("[\\\\.\\[\\]{}()<>*+\\-=!?^$|]");
|
||||
return regexSpecialCharacters.matcher(input).find();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
package net.knarcraft.ffmpegconverter.handler;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.FFMpegConvert;
|
||||
import net.knarcraft.ffmpegconverter.config.ConfigHandler;
|
||||
import net.knarcraft.ffmpegconverter.config.ConfigKey;
|
||||
import net.knarcraft.ffmpegconverter.utility.OutputUtil;
|
||||
import org.apache.commons.configuration2.PropertiesConfiguration;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A handler for keeping track of which hardware acceleration methods are available on the current system
|
||||
*/
|
||||
public record AvailableHardwareEncoderHandler(@NotNull List<String> availableHardwareEncodings) {
|
||||
|
||||
private static final ConfigHandler configHandler = FFMpegConvert.getConfiguration().getConfigHandler();
|
||||
|
||||
/**
|
||||
* Gets all hardware encodings
|
||||
*
|
||||
* @return <p>All hardware encodings</p>
|
||||
*/
|
||||
@Override
|
||||
@NotNull
|
||||
public List<String> availableHardwareEncodings() {
|
||||
return new ArrayList<>(this.availableHardwareEncodings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the specified hardware encoding
|
||||
*
|
||||
* @param encoding <p>The hardware encoding to remove</p>
|
||||
*/
|
||||
public void removeHardwareEncoding(@NotNull String encoding) {
|
||||
this.availableHardwareEncodings.remove(encoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves settings for this available hardware encoder handler
|
||||
*/
|
||||
public void save() {
|
||||
PropertiesConfiguration configuration = configHandler.getWritableConfiguration();
|
||||
configuration.setProperty(ConfigKey.HARDWARE_ACCELERATED_ENCODERS.toString(), String.join(",", this.availableHardwareEncodings));
|
||||
configHandler.writeConfiguration();
|
||||
OutputUtil.printDebug("Saved available hardware encoder handler");
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads saved settings for an available hardware encoder handler
|
||||
*
|
||||
* @return <p>The loaded available hardware encoder handler, or a new one if no data has been saved</p>
|
||||
*/
|
||||
@NotNull
|
||||
public static AvailableHardwareEncoderHandler load() {
|
||||
try {
|
||||
configHandler.load();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return new AvailableHardwareEncoderHandler(FFMpegConvert.getConfiguration().hardwareEncoders());
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package net.knarcraft.ffmpegconverter.parser;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.utility.ListUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A class representing a command argument
|
||||
@ -10,16 +11,18 @@ public class ConverterArgument {
|
||||
private final String name;
|
||||
private final char shorthand;
|
||||
private final boolean valueRequired;
|
||||
private final ConverterArgumentValue valueType;
|
||||
private final ConverterArgumentValueType valueType;
|
||||
|
||||
/**
|
||||
* Instantiates a converter argument
|
||||
*
|
||||
* @param name <p>The name of the argument which users has to type.</p>
|
||||
* @param shorthand <p>A single character value for using the command.</p>
|
||||
* @param valueRequired <p>Whether the argument must be followed by a valid value.</p>
|
||||
* @param valueType <p>The type of value the argument requires.</p>
|
||||
*/
|
||||
public ConverterArgument(String name, char shorthand, boolean valueRequired, ConverterArgumentValue valueType) {
|
||||
public ConverterArgument(@NotNull String name, char shorthand, boolean valueRequired,
|
||||
@NotNull ConverterArgumentValueType valueType) {
|
||||
this.name = name;
|
||||
this.shorthand = shorthand;
|
||||
this.valueRequired = valueRequired;
|
||||
@ -28,6 +31,7 @@ public class ConverterArgument {
|
||||
|
||||
/**
|
||||
* Gets the argument name
|
||||
*
|
||||
* @return <p>The argument name.</p>
|
||||
*/
|
||||
public String getName() {
|
||||
@ -36,6 +40,7 @@ public class ConverterArgument {
|
||||
|
||||
/**
|
||||
* Gets the argument shorthand
|
||||
*
|
||||
* @return <p>The argument shorthand</p>
|
||||
*/
|
||||
public char getShorthand() {
|
||||
@ -44,6 +49,7 @@ public class ConverterArgument {
|
||||
|
||||
/**
|
||||
* Gets whether the argument requires a value
|
||||
*
|
||||
* @return <p>Whether the argument requires a value.</p>
|
||||
*/
|
||||
public boolean isValueRequired() {
|
||||
@ -56,8 +62,8 @@ public class ConverterArgument {
|
||||
* @param value <p>The value to test.</p>
|
||||
* @return <p>True if the argument is valid. False otherwise.</p>
|
||||
*/
|
||||
public boolean testArgumentValue(String value) {
|
||||
if (value.length() == 0) {
|
||||
public boolean testArgumentValue(@NotNull String value) {
|
||||
if (value.isEmpty()) {
|
||||
return !valueRequired;
|
||||
}
|
||||
if (valueRequired && value.startsWith("-")) {
|
||||
@ -73,9 +79,14 @@ public class ConverterArgument {
|
||||
case STRING:
|
||||
return true;
|
||||
case INT:
|
||||
int ignored = Integer.parseInt(value);
|
||||
try {
|
||||
Integer.parseInt(value);
|
||||
return true;
|
||||
}
|
||||
} catch (NumberFormatException exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,8 +0,0 @@
|
||||
package net.knarcraft.ffmpegconverter.parser;
|
||||
|
||||
public enum ConverterArgumentValue {
|
||||
BOOLEAN,
|
||||
COMMA_SEPARATED_LIST,
|
||||
STRING,
|
||||
INT
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package net.knarcraft.ffmpegconverter.parser;
|
||||
|
||||
/**
|
||||
* The value types converter arguments can have
|
||||
*/
|
||||
public enum ConverterArgumentValueType {
|
||||
|
||||
/**
|
||||
* A boolean value
|
||||
*/
|
||||
BOOLEAN,
|
||||
|
||||
/**
|
||||
* A list separated by commas
|
||||
*/
|
||||
COMMA_SEPARATED_LIST,
|
||||
|
||||
/**
|
||||
* A normal string
|
||||
*/
|
||||
STRING,
|
||||
|
||||
/**
|
||||
* An integer
|
||||
*/
|
||||
INT
|
||||
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package net.knarcraft.ffmpegconverter.property;
|
||||
|
||||
/**
|
||||
* A representation for different preferences related to minimal subtitles
|
||||
*
|
||||
* <p>Minimal subtitles are also referred to as partial subtitles or signs and songs. For Japanese media, they are aimed
|
||||
* at users that understand the spoken language, but struggles with reading, or when things are said too fast or in an
|
||||
* odd rhythm (singing). In american movies, some partial subtitles only translate non-english spoken language. Some
|
||||
* dubbed movies have subtitles that only cover signs, text or logos in the original language.</p>
|
||||
*/
|
||||
public enum MinimalSubtitlePreference {
|
||||
|
||||
/**
|
||||
* Only map minimal subtitles
|
||||
*/
|
||||
REQUIRE,
|
||||
|
||||
/**
|
||||
* Prefer minimal subtitles when available
|
||||
*/
|
||||
PREFER,
|
||||
|
||||
/**
|
||||
* Don't do any changes in sorting based on minimal subtitles
|
||||
*/
|
||||
NO_PREFERENCE,
|
||||
|
||||
/**
|
||||
* Avoid minimal subtitles, unless it's the only available choice
|
||||
*/
|
||||
AVOID,
|
||||
|
||||
/**
|
||||
* Don't include minimal subtitles, no matter what
|
||||
*/
|
||||
REJECT,
|
||||
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package net.knarcraft.ffmpegconverter.property;
|
||||
|
||||
/**
|
||||
* A representation of different stream types
|
||||
*/
|
||||
public enum StreamType {
|
||||
|
||||
/**
|
||||
* A video stream
|
||||
*/
|
||||
VIDEO,
|
||||
|
||||
/**
|
||||
* An audio stream
|
||||
*/
|
||||
AUDIO,
|
||||
|
||||
/**
|
||||
* A subtitle stream
|
||||
*/
|
||||
SUBTITLE,
|
||||
|
||||
/**
|
||||
* A cover image
|
||||
*
|
||||
* <p>Cover images are treated as video streams by ffmpeg, so they need special treatment</p>
|
||||
*/
|
||||
COVER_IMAGE,
|
||||
|
||||
/**
|
||||
* Binary data
|
||||
*
|
||||
* <p>Binary data streams only cause problems, as they cannot, for example, be included in an MKV file.</p>
|
||||
*/
|
||||
DATA,
|
||||
|
||||
/**
|
||||
* None of the above
|
||||
*/
|
||||
OTHER
|
||||
|
||||
}
|
@ -1,15 +1,44 @@
|
||||
package net.knarcraft.ffmpegconverter.streams;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.utility.ValueParsingHelper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* An abstract implementation of a stream object implementing common methods
|
||||
*/
|
||||
public abstract class AbstractStream implements StreamObject {
|
||||
int absoluteIndex;
|
||||
int relativeIndex;
|
||||
String codecName;
|
||||
String language;
|
||||
|
||||
protected final int inputIndex;
|
||||
protected final int absoluteIndex;
|
||||
protected final int relativeIndex;
|
||||
protected final String codecName;
|
||||
protected final String language;
|
||||
protected final boolean isDefault;
|
||||
protected final String title;
|
||||
protected int outputIndex;
|
||||
|
||||
/**
|
||||
* Instantiates a new abstract stream
|
||||
*
|
||||
* @param streamInfo <p>All info about the stream</p>
|
||||
* @param inputIndex <p>The index of the input file this stream belongs to</p>
|
||||
* @param relativeIndex <p>The relative index of this stream, only considering streams of the same type</p>
|
||||
*/
|
||||
protected AbstractStream(@NotNull Map<StreamTag, String> streamInfo, int inputIndex, int relativeIndex) {
|
||||
this.codecName = ValueParsingHelper.parseString(streamInfo.get(StreamTag.CODEC_NAME), "");
|
||||
this.absoluteIndex = ValueParsingHelper.parseInt(streamInfo.get(StreamTag.INDEX), -1);
|
||||
this.language = parseLanguage(streamInfo);
|
||||
this.isDefault = ValueParsingHelper.parseBoolean(streamInfo.get(StreamTag.DISPOSITION_DEFAULT), false);
|
||||
this.title = ValueParsingHelper.parseString(streamInfo.get(StreamTag.TAG_TITLE), "");
|
||||
this.inputIndex = inputIndex;
|
||||
this.relativeIndex = relativeIndex;
|
||||
this.outputIndex = -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String getCodecName() {
|
||||
return this.codecName;
|
||||
}
|
||||
@ -25,8 +54,64 @@ public abstract class AbstractStream implements StreamObject {
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String getLanguage() {
|
||||
return this.language;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDefault() {
|
||||
return this.isDefault;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String getTitle() {
|
||||
return this.title;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInputIndex() {
|
||||
return this.inputIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOutputIndex() {
|
||||
return this.outputIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOutputIndex(int newIndex) {
|
||||
this.outputIndex = newIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the correct language of a stream
|
||||
*
|
||||
* <p>As some people tend to set weird incorrect languages for anime streams, this tries to correct that by
|
||||
* detecting streams that are actually in english, but set to something else.</p>
|
||||
*
|
||||
* @param streamInfo <p>All info about the stream</p>
|
||||
* @return <p>The actual language</p>
|
||||
*/
|
||||
@NotNull
|
||||
private String parseLanguage(@NotNull Map<StreamTag, String> streamInfo) {
|
||||
String languageString = ValueParsingHelper.parseString(streamInfo.get(StreamTag.TAG_LANGUAGE), "und");
|
||||
String title = ValueParsingHelper.parseString(streamInfo.get(StreamTag.TAG_TITLE), "");
|
||||
if (languageString.equalsIgnoreCase("zxx") || languageString.equalsIgnoreCase("enm") ||
|
||||
(title.toLowerCase().matches(".*english.*") && languageString.equalsIgnoreCase("jpn"))) {
|
||||
return "eng";
|
||||
}
|
||||
if (languageString.equalsIgnoreCase("jap")) {
|
||||
return "jpn";
|
||||
}
|
||||
return languageString;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String toString() {
|
||||
return this.title + " | Input index: " + inputIndex + " | Language: " + language;
|
||||
}
|
||||
|
||||
}
|
@ -1,30 +1,38 @@
|
||||
package net.knarcraft.ffmpegconverter.streams;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.utility.ValueParsingHelper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* This class represents an ffmpeg audio stream
|
||||
* This class represents a ffmpeg audio stream
|
||||
*/
|
||||
public class AudioStream extends AbstractStream implements StreamObject {
|
||||
|
||||
private final int channels;
|
||||
private final String title;
|
||||
private final boolean isSpecialAudio;
|
||||
|
||||
/**
|
||||
* Instantiates a new audio stream
|
||||
*
|
||||
* @param codecName <p>The codec of the audio stream.</p>
|
||||
* @param absoluteIndex <p>The index of the audio stream.</p>
|
||||
* @param relativeIndex <p>The index of the audio stream relative to other audio streams.</p>
|
||||
* @param language <p>The language of the audio stream.</p>
|
||||
* @param title <p>The title of the audio stream.</p>
|
||||
* @param channels <p>The number of channels for the audio stream.</p>
|
||||
* @param streamInfo <p>All info about the stream</p>
|
||||
* @param inputIndex <p>The index of the input file containing this stream</p>
|
||||
* @param relativeIndex <p>The index of the audio stream relative to other audio streams</p>
|
||||
*/
|
||||
public AudioStream(String codecName, int absoluteIndex, int relativeIndex, String language, String title,
|
||||
int channels) {
|
||||
this.codecName = codecName;
|
||||
this.absoluteIndex = absoluteIndex;
|
||||
this.language = language;
|
||||
this.title = title;
|
||||
this.relativeIndex = relativeIndex;
|
||||
this.channels = channels;
|
||||
public AudioStream(@NotNull Map<StreamTag, String> streamInfo, int inputIndex, int relativeIndex) {
|
||||
super(streamInfo, inputIndex, relativeIndex);
|
||||
this.channels = ValueParsingHelper.parseInt(streamInfo.get(StreamTag.CHANNELS), 0);
|
||||
this.isSpecialAudio = checkIfIsSpecialAudio();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether this audio stream is a special audio stream such as audio description or music only
|
||||
*
|
||||
* @return <p>True if this audio stream is a special audio stream</p>
|
||||
*/
|
||||
public boolean isSpecialAudio() {
|
||||
return this.isSpecialAudio;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -36,12 +44,19 @@ public class AudioStream extends AbstractStream implements StreamObject {
|
||||
return this.channels;
|
||||
}
|
||||
|
||||
@Override
|
||||
public char streamTypeCharacter() {
|
||||
return 'a';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the title of the audio stream
|
||||
* Checks whether this audio stream is a special audio stream
|
||||
*
|
||||
* @return <p>The title of the audio stream.</p>
|
||||
* @return <p>True if this is a special audio stream</p>
|
||||
*/
|
||||
public String getTitle() {
|
||||
return this.title;
|
||||
private boolean checkIfIsSpecialAudio() {
|
||||
String titleLowercase = getTitle().toLowerCase().trim();
|
||||
return titleLowercase.matches(".*audio description.*");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,40 @@
|
||||
package net.knarcraft.ffmpegconverter.streams;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A stream type for attached streams that aren't audio, video or subtitles
|
||||
*/
|
||||
public class OtherStream extends AbstractStream {
|
||||
|
||||
private final boolean isCoverImage;
|
||||
|
||||
/**
|
||||
* Instantiates a new other stream
|
||||
*
|
||||
* @param streamInfo <p>All info about the stream</p>
|
||||
* @param inputIndex <p>The index of the input file this stream belongs to</p>
|
||||
* @param isCoverImage <p>Whether this stream is a cover image stream</p>
|
||||
*/
|
||||
public OtherStream(@NotNull Map<StreamTag, String> streamInfo, int inputIndex, boolean isCoverImage) {
|
||||
super(streamInfo, inputIndex, 0);
|
||||
this.isCoverImage = isCoverImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether this stream is a cover image stream
|
||||
*
|
||||
* @return <p>True if this stream is a cover image stream</p>
|
||||
*/
|
||||
public boolean isCoverImage() {
|
||||
return isCoverImage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public char streamTypeCharacter() {
|
||||
return '?';
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,11 @@
|
||||
package net.knarcraft.ffmpegconverter.streams;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* An object describing a generic video file stream
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public interface StreamObject {
|
||||
|
||||
/**
|
||||
@ -7,6 +13,7 @@ public interface StreamObject {
|
||||
*
|
||||
* @return <p>Codec name.</p>
|
||||
*/
|
||||
@NotNull
|
||||
String getCodecName();
|
||||
|
||||
/**
|
||||
@ -16,6 +23,15 @@ public interface StreamObject {
|
||||
*/
|
||||
int getAbsoluteIndex();
|
||||
|
||||
/**
|
||||
* Gets the index of the input file this stream belongs to
|
||||
*
|
||||
* <p>This is the first number given to a map argument in order to map this stream to the output file.</p>
|
||||
*
|
||||
* @return <p>The input index of this stream</p>
|
||||
*/
|
||||
int getInputIndex();
|
||||
|
||||
/**
|
||||
* Gets the relative index of a stream object (kth element of codec type)
|
||||
*
|
||||
@ -24,10 +40,50 @@ public interface StreamObject {
|
||||
int getRelativeIndex();
|
||||
|
||||
/**
|
||||
* Gets the language of the audio stream
|
||||
* Gets the language of the audio or subtitle stream
|
||||
*
|
||||
* @return <p>The language of the audio stream.</p>
|
||||
* @return <p>The language of the audio or subtitle stream.</p>
|
||||
*/
|
||||
@NotNull
|
||||
String getLanguage();
|
||||
|
||||
/**
|
||||
* Gets whether this stream is marked as the default stream of its type
|
||||
*
|
||||
* @return <p>True if this stream is marked as default</p>
|
||||
*/
|
||||
boolean isDefault();
|
||||
|
||||
/**
|
||||
* Gets the title of the subtitle stream
|
||||
*
|
||||
* @return <p>The title of the subtitle stream.</p>
|
||||
*/
|
||||
@NotNull
|
||||
String getTitle();
|
||||
|
||||
/**
|
||||
* Gets the character ffmpeg uses for this type of stream
|
||||
*
|
||||
* @return <p>The character used to specify this type of stream</p>
|
||||
*/
|
||||
char streamTypeCharacter();
|
||||
|
||||
/**
|
||||
* Gets the index of this stream in the output file
|
||||
*
|
||||
* @return <p>The index of this stream in the output file, or -1 if not set</p>
|
||||
*/
|
||||
int getOutputIndex();
|
||||
|
||||
/**
|
||||
* Sets the index of this stream in the output file
|
||||
*
|
||||
* <p>After sorting and selecting streams, setting the output index makes it much easier to set properties for this
|
||||
* specific stream, without having to rely on loops</p>
|
||||
*
|
||||
* @param newIndex <p>The new output index</p>
|
||||
*/
|
||||
void setOutputIndex(int newIndex);
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,504 @@
|
||||
package net.knarcraft.ffmpegconverter.streams;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A class for representing stream tags that might be found for streams in video files
|
||||
*/
|
||||
public enum StreamTag {
|
||||
|
||||
/**
|
||||
* The absolute index of this stream in the file
|
||||
*
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
INDEX("index"),
|
||||
/**
|
||||
* The name of the codec, useful for identification
|
||||
*
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
CODEC_NAME("codec_name"),
|
||||
|
||||
/**
|
||||
* The long name of the codec, useful for displaying information
|
||||
*
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
CODEC_LONG_NAME("codec_long_name"),
|
||||
|
||||
/**
|
||||
* The profile the encoder is set to
|
||||
*
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
PROFILE("profile"),
|
||||
|
||||
/**
|
||||
* Whether the type of codec for the stream is audio, video or subtitle
|
||||
*
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
CODEC_TYPE("codec_type"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
CODEC_TAG_STRING("codec_tag_string"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
CODEC_TAG("codec_tag"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for audio streams</p>
|
||||
*/
|
||||
SAMPLE_FMT("sample_fmt"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for audio streams</p>
|
||||
*/
|
||||
SAMPLE_RATE("sample_rate"),
|
||||
|
||||
/**
|
||||
* The number of channels in an audio stream
|
||||
*
|
||||
* <p>Applicable for audio streams</p>
|
||||
*/
|
||||
CHANNELS("channels"),
|
||||
|
||||
/**
|
||||
* Human-recognizable term for the number of audio channels, such as stereo, mono or surround
|
||||
*
|
||||
* <p>Applicable for audio streams</p>
|
||||
*/
|
||||
CHANNEL_LAYOUT("channel_layout"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for audio streams</p>
|
||||
*/
|
||||
BITS_PER_SAMPLE("bits_per_sample"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for audio streams</p>
|
||||
*/
|
||||
INITIAL_PADDING("initial_padding"),
|
||||
|
||||
/**
|
||||
* The viewable video width
|
||||
*
|
||||
* <p>Applicable for video and subtitle streams</p>
|
||||
*/
|
||||
WIDTH("width"),
|
||||
|
||||
/**
|
||||
* The viewable video height
|
||||
*
|
||||
* <p>Applicable for video and subtitle streams</p>
|
||||
*/
|
||||
HEIGHT("height"),
|
||||
|
||||
/**
|
||||
* The original video width, before any padding was applied to account for resolution multiples
|
||||
*
|
||||
* <p>Applicable for video streams</p>
|
||||
*/
|
||||
CODED_WIDTH("coded_width"),
|
||||
|
||||
/**
|
||||
* The original video height, before any padding was applied to account for resolution multiples
|
||||
*
|
||||
* <p>Applicable for video streams</p>
|
||||
*/
|
||||
CODED_HEIGHT("coded_height"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for video streams</p>
|
||||
*/
|
||||
CLOSED_CAPTIONS("closed_captions"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for video streams</p>
|
||||
*/
|
||||
FILM_GRAIN("film_grain"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for video streams</p>
|
||||
*/
|
||||
HAS_B_FRAMES("has_b_frames"),
|
||||
|
||||
/**
|
||||
* The aspect ratio used to stretch the video for playback
|
||||
*
|
||||
* <p>Applicable for video streams</p>
|
||||
*/
|
||||
SAMPLE_ASPECT_RATIO("sample_aspect_ratio"),
|
||||
|
||||
/**
|
||||
* The aspect ratio of the video stream
|
||||
*
|
||||
* <p>Applicable for video streams</p>
|
||||
*/
|
||||
DISPLAY_ASPECT_RATIO("display_aspect_ratio"),
|
||||
|
||||
/**
|
||||
* The pixel format used for the video stream
|
||||
*
|
||||
* <p>Applicable for video streams</p>
|
||||
*/
|
||||
PIX_FMT("pix_fmt"),
|
||||
|
||||
/**
|
||||
* The quality level of the video stream
|
||||
*
|
||||
* <p>Applicable for video streams</p>
|
||||
*/
|
||||
LEVEL("level"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for video streams</p>
|
||||
*/
|
||||
COLOR_RANGE("color_range"),
|
||||
|
||||
/**
|
||||
* How colors are stored in the video stream's file
|
||||
*
|
||||
* <p>Applicable for video streams</p>
|
||||
*/
|
||||
COLOR_SPACE("color_space"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for video streams</p>
|
||||
*/
|
||||
COLOR_TRANSFER("color_transfer"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for video streams</p>
|
||||
*/
|
||||
COLOR_PRIMARIES("color_primaries"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for video streams</p>
|
||||
*/
|
||||
CHROMA_LOCATION("chroma_location"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for video streams</p>
|
||||
*/
|
||||
FIELD_ORDER("field_order"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for video streams</p>
|
||||
*/
|
||||
REFS("refs"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for video streams</p>
|
||||
*/
|
||||
IS_AVC("is_avc"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for video streams</p>
|
||||
*/
|
||||
NAL_LENGTH_SIZE("nal_length_size"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
ID("id"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
R_FRAME_RATE("r_frame_rate"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
AVERAGE_FRAME_RATE("avg_frame_rate"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
TIME_BASE("time_base"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
START_PTS("start_pts"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
START_TIME("start_time"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
DURATION_TS("duration_ts"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
DURATION("duration"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
BIT_RATE("bit_rate"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
MAX_BIT_RATE("max_bit_rate"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
BITS_PER_RAW_SAMPLE("bits_per_raw_sample"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
NB_FRAMES("nb_frames"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
NB_READ_FRAMES("nb_read_frames"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
NB_READ_PACKETS("nb_read_packets"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for video and subtitle streams</p>
|
||||
*/
|
||||
EXTRA_DATA_SIZE("extradata_size"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
DISPOSITION_DEFAULT("DISPOSITION:default"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
DISPOSITION_DUB("DISPOSITION:dub"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
DISPOSITION_ORIGINAL("DISPOSITION:original"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
DISPOSITION_COMMENT("DISPOSITION:comment"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
DISPOSITION_LYRICS("DISPOSITION:lyrics"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
DISPOSITION_KARAOKE("DISPOSITION:karaoke"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
DISPOSITION_FORCED("DISPOSITION:forced"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
DISPOSITION_HEARING_IMPAIRED("DISPOSITION:hearing_impaired"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
DISPOSITION_VISUAL_IMPAIRED("DISPOSITION:visual_impaired"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
DISPOSITION_CLEAN_EFFECTS("DISPOSITION:clean_effects"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
DISPOSITION_ATTACHED_PIC("DISPOSITION:attached_pic"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
DISPOSITION_TIMED_THUMBNAILS("DISPOSITION:timed_thumbnails"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
DISPOSITION_NON_DIEGETIC("DISPOSITION:non_diegetic"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
DISPOSITION_CAPTIONS("DISPOSITION:captions"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
DISPOSITION_DESCRIPTIONS("DISPOSITION:descriptions"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
DISPOSITION_METADATA("DISPOSITION:metadata"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
DISPOSITION_DEPENDENT("DISPOSITION:dependent"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
DISPOSITION_STILL_IMAGE("DISPOSITION:still_image"),
|
||||
|
||||
/**
|
||||
* The language of the stream
|
||||
*
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
TAG_LANGUAGE("TAG:language"),
|
||||
|
||||
/**
|
||||
* The title of an audio stream
|
||||
*
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
TAG_TITLE("TAG:title"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
TAG_BPS_ENG("TAG:BPS-eng"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
TAG_DURATION_ENG("TAG:DURATION-eng"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
TAG_NUMBER_OF_FRAMES_ENG("TAG:NUMBER_OF_FRAMES-eng"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
TAG_NUMBER_OF_BYTES_ENG("TAG:NUMBER_OF_BYTES-eng"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
TAG_SOURCE_ID_ENG("TAG:SOURCE_ID-eng"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
TAG_SOURCE_ID("TAG:SOURCE_ID"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
TAG_STATISTICS_WRITING_APP_ENG("TAG:_STATISTICS_WRITING_APP-eng"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
TAG_STATISTICS_WRITING_APP("TAG:_STATISTICS_WRITING_APP"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
TAG_STATISTICS_WRITING_DATE_UTC_ENG("TAG:_STATISTICS_WRITING_DATE_UTC-eng"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
TAG_STATISTICS_WRITING_DATE_UTC("TAG:_STATISTICS_WRITING_DATE_UTC"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
TAG_STATISTICS_TAGS_ENG("TAG:_STATISTICS_TAGS-eng"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
TAG_STATISTICS_TAGS("TAG:_STATISTICS_TAGS"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for video streams</p>
|
||||
*/
|
||||
TAG_ENCODER("TAG:ENCODER"),
|
||||
|
||||
/**
|
||||
* <p>Applicable for all 3 stream types</p>
|
||||
*/
|
||||
TAG_DURATION("TAG:DURATION"),
|
||||
|
||||
/**
|
||||
* The file name of the attached file (image/font)
|
||||
*/
|
||||
TAG_FILE_NAME("TAG:filename"),
|
||||
|
||||
/**
|
||||
* The mime type of the attached file (image/font)
|
||||
*/
|
||||
TAG_MIME_TYPE("TAG:mimetype"),
|
||||
;
|
||||
|
||||
private static final Map<String, StreamTag> tagLookup = new HashMap<>();
|
||||
|
||||
private final @NotNull String tagString;
|
||||
|
||||
/**
|
||||
* Instantiates a new stream tag
|
||||
*
|
||||
* @param tagString <p>The tag string ffmpeg prints to specify this tag</p>
|
||||
*/
|
||||
StreamTag(@NotNull String tagString) {
|
||||
this.tagString = tagString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the stream tag defined by the given string
|
||||
*
|
||||
* @param input <p>The input string to parse</p>
|
||||
* @return <p>The corresponding stream tab, or null if not found</p>
|
||||
*/
|
||||
@Nullable
|
||||
public static StreamTag getFromString(@NotNull String input) {
|
||||
if (tagLookup.isEmpty()) {
|
||||
for (StreamTag tag : StreamTag.values()) {
|
||||
tagLookup.put(tag.tagString, tag);
|
||||
}
|
||||
}
|
||||
return tagLookup.get(input);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.tagString;
|
||||
}
|
||||
|
||||
}
|
@ -1,52 +1,30 @@
|
||||
package net.knarcraft.ffmpegconverter.streams;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.utility.SubtitleHelper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* An object representation of a subtitle stream in a media file
|
||||
*/
|
||||
public class SubtitleStream extends AbstractStream implements StreamObject {
|
||||
final private String title;
|
||||
final private String file;
|
||||
final private boolean isFullSubtitle;
|
||||
final private boolean isImageSubtitle;
|
||||
|
||||
private final boolean isFullSubtitle;
|
||||
private final boolean isImageSubtitle;
|
||||
|
||||
/**
|
||||
* Instantiates a new subtitle stream
|
||||
*
|
||||
* @param codecName <p>The name of the codec for the subtitle stream.</p>
|
||||
* @param absoluteIndex <p>The index of the subtitle stream.</p>
|
||||
* @param relativeIndex <p>The index of the subtitle stream relative to other subtitle streams.</p>
|
||||
* @param language <p>The language of the subtitle stream.</p>
|
||||
* @param title <p>The title of the subtitle stream.</p>
|
||||
* @param file <p>The file containing the subtitle.</p>
|
||||
* @param streamInfo <p>All info about the stream</p>
|
||||
* @param inputIndex <p>The index of the input file containing this stream</p>
|
||||
* @param relativeIndex <p>The index of the subtitle stream relative to other subtitle streams</p>
|
||||
*/
|
||||
public SubtitleStream(String codecName, int absoluteIndex, int relativeIndex, String language, String title,
|
||||
String file) {
|
||||
this.codecName = codecName;
|
||||
this.absoluteIndex = absoluteIndex;
|
||||
this.language = language;
|
||||
this.title = title;
|
||||
this.isFullSubtitle = isFullSubtitle();
|
||||
this.relativeIndex = relativeIndex;
|
||||
this.isImageSubtitle = isImageSubtitle();
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the title of the subtitle stream
|
||||
*
|
||||
* @return <p>The title of the subtitle stream.</p>
|
||||
*/
|
||||
private String getTitle() {
|
||||
return this.title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the file name of the file containing this subtitle
|
||||
*
|
||||
* @return <p>The file name containing the subtitle stream.</p>
|
||||
*/
|
||||
public String getFile() {
|
||||
return this.file;
|
||||
public SubtitleStream(@NotNull Map<StreamTag, String> streamInfo, int inputIndex, int relativeIndex) {
|
||||
super(streamInfo, inputIndex, relativeIndex);
|
||||
this.isFullSubtitle = checkIfIsFullSubtitle();
|
||||
this.isImageSubtitle = codecName != null &&
|
||||
(getCodecName().equals("hdmv_pgs_subtitle") || getCodecName().equals("dvd_subtitle"));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -54,7 +32,7 @@ public class SubtitleStream extends AbstractStream implements StreamObject {
|
||||
*
|
||||
* @return <p>Whether the subtitles is an image subtitle.</p>
|
||||
*/
|
||||
public boolean getIsImageSubtitle() {
|
||||
public boolean isImageSubtitle() {
|
||||
return this.isImageSubtitle;
|
||||
}
|
||||
|
||||
@ -63,32 +41,28 @@ public class SubtitleStream extends AbstractStream implements StreamObject {
|
||||
*
|
||||
* @return <p>Whether the subtitle is a full subtitle.</p>
|
||||
*/
|
||||
public boolean getIsFullSubtitle() {
|
||||
public boolean isFullSubtitle() {
|
||||
return this.isFullSubtitle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a subtitle is image based (as opposed to text based)
|
||||
*
|
||||
* @return <p>True if the subtitle is image based.</p>
|
||||
*/
|
||||
private boolean isImageSubtitle() {
|
||||
return codecName != null && (getCodecName().equals("hdmv_pgs_subtitle")
|
||||
|| getCodecName().equals("dvd_subtitle"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the subtitle translates everything (as opposed to just songs and signs)
|
||||
*
|
||||
* @return <p>True if the subtitle translates everything.</p>
|
||||
*/
|
||||
private boolean isFullSubtitle() {
|
||||
if (getTitle() == null) {
|
||||
return false;
|
||||
private boolean checkIfIsFullSubtitle() {
|
||||
return !SubtitleHelper.isSongsSignsSubtitle(this.getTitle());
|
||||
}
|
||||
String titleLowercase = getTitle().toLowerCase();
|
||||
return !titleLowercase.matches("signs?[ &\\/a-z]+songs?") &&
|
||||
!titleLowercase.matches("songs?[ &\\/a-z]+signs?") &&
|
||||
!titleLowercase.matches("forced");
|
||||
|
||||
@Override
|
||||
public char streamTypeCharacter() {
|
||||
return 's';
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String toString() {
|
||||
return super.toString() + " | Is full: " + this.isFullSubtitle;
|
||||
}
|
||||
|
||||
}
|
@ -1,27 +1,29 @@
|
||||
package net.knarcraft.ffmpegconverter.streams;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.utility.ValueParsingHelper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* An object representation of a video stream in a media file
|
||||
*/
|
||||
public class VideoStream extends AbstractStream implements StreamObject {
|
||||
|
||||
private final int width;
|
||||
private final int height;
|
||||
|
||||
/**
|
||||
* Instantiates a new video stream
|
||||
*
|
||||
* @param codec <p>The name of the codec for the video stream.</p>
|
||||
* @param absoluteIndex <p>The index of the video stream.</p>
|
||||
* @param relativeIndex <p>The index of the video stream relative to other video streams.</p>
|
||||
* @param width <p>The width of the video stream.</p>
|
||||
* @param height <p>The height of the video stream.</p>
|
||||
* @param streamInfo <p>All info about the stream</p>
|
||||
* @param inputIndex <p>The index of the input file containing this stream</p>
|
||||
* @param relativeIndex <p>The index of the video stream relative to other video streams</p>
|
||||
*/
|
||||
public VideoStream(String codec, int absoluteIndex, int relativeIndex, int width, int height) {
|
||||
this.codecName = codec;
|
||||
this.absoluteIndex = absoluteIndex;
|
||||
this.relativeIndex = relativeIndex;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
public VideoStream(@NotNull Map<StreamTag, String> streamInfo, int inputIndex, int relativeIndex) {
|
||||
super(streamInfo, inputIndex, relativeIndex);
|
||||
this.width = ValueParsingHelper.parseInt(streamInfo.get(StreamTag.WIDTH), -1);
|
||||
this.height = ValueParsingHelper.parseInt(streamInfo.get(StreamTag.HEIGHT), -1);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -41,4 +43,10 @@ public class VideoStream extends AbstractStream implements StreamObject {
|
||||
public int getHeight() {
|
||||
return this.height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public char streamTypeCharacter() {
|
||||
return 'v';
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package net.knarcraft.ffmpegconverter.utility;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A helper class for dealing with configuration value types
|
||||
*/
|
||||
public final class ConfigHelper {
|
||||
|
||||
private ConfigHelper() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the given value as a string list
|
||||
*
|
||||
* @param value <p>The raw string list value</p>
|
||||
* @return <p>The value as a string list, or null if not compatible</p>
|
||||
*/
|
||||
public static @NotNull List<String> asStringList(@Nullable Object value) {
|
||||
if (value == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
if (value instanceof String string) {
|
||||
return List.of((string).split(","));
|
||||
} else if (value instanceof List<?> list) {
|
||||
List<String> strings = new ArrayList<>();
|
||||
for (Object object : list) {
|
||||
strings.add(String.valueOf(object));
|
||||
}
|
||||
return strings;
|
||||
} else {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the given value as a boolean
|
||||
*
|
||||
* <p>This will throw an exception if used for a non-boolean value</p>
|
||||
*
|
||||
* @param value <p>The object value to get</p>
|
||||
* @return <p>The value of the given object as a boolean</p>
|
||||
* @throws ClassCastException <p>If the given value is not a boolean</p>
|
||||
*/
|
||||
public static boolean asBoolean(@Nullable Object value) throws ClassCastException {
|
||||
if (value instanceof Boolean booleanValue) {
|
||||
return booleanValue;
|
||||
} else if (value instanceof String) {
|
||||
return Boolean.parseBoolean((String) value);
|
||||
} else {
|
||||
throw new ClassCastException();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,9 +1,18 @@
|
||||
package net.knarcraft.ffmpegconverter.utility;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.FFMpegConvert;
|
||||
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||
import net.knarcraft.ffmpegconverter.container.ProcessResult;
|
||||
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
|
||||
import net.knarcraft.ffmpegconverter.property.StreamType;
|
||||
import net.knarcraft.ffmpegconverter.streams.AudioStream;
|
||||
import net.knarcraft.ffmpegconverter.streams.OtherStream;
|
||||
import net.knarcraft.ffmpegconverter.streams.StreamObject;
|
||||
import net.knarcraft.ffmpegconverter.streams.StreamTag;
|
||||
import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
|
||||
import net.knarcraft.ffmpegconverter.streams.VideoStream;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
@ -11,7 +20,9 @@ import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A class which helps with ffmpeg probing and converting
|
||||
@ -21,87 +32,83 @@ public final class FFMpegHelper {
|
||||
private static final String PROBE_SPLIT_CHARACTER = "øæåÆØå";
|
||||
|
||||
private FFMpegHelper() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets streams from a file
|
||||
*
|
||||
* @param ffprobePath <p>The path/command to ffprobe.</p>
|
||||
* @param file <p>The file to probe.</p>
|
||||
* @return <p>A list of StreamObjects.</p>
|
||||
* @throws IOException <p>If the process can't be readProcess.</p>
|
||||
* @param ffprobePath <p>The path/command to ffprobe</p>
|
||||
* @param file <p>The file to probe</p>
|
||||
* @param subtitleFormats <p>The extensions to accept for external subtitles</p>
|
||||
* @return <p>A list of StreamObjects</p>
|
||||
* @throws IOException <p>If the process can't be readProcess</p>
|
||||
*/
|
||||
public static List<StreamObject> probeFile(String ffprobePath, File file) throws IOException {
|
||||
return parseStreams(ffprobePath, probeForStreams(ffprobePath, file), file);
|
||||
@NotNull
|
||||
public static StreamProbeResult probeFile(@NotNull String ffprobePath, @NotNull File file,
|
||||
@NotNull List<String> subtitleFormats) throws IOException {
|
||||
return parseStreams(ffprobePath, probeForStreams(ffprobePath, file), file, subtitleFormats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list containing all required arguments for converting a video to a web playable video
|
||||
*
|
||||
* @param executable <p>The executable to use (ffmpeg/ffprobe).</p>
|
||||
* @param fileName <p>The name of the file to execute on.</p>
|
||||
* @return <p>A base list of ffmpeg commands for converting a video for web</p>
|
||||
* @param executable <p>The executable to use (ffmpeg/ffprobe)</p>
|
||||
* @param files <p>The files to execute on</p>
|
||||
* @return <p>A FFMPEG command for web-playable video</p>
|
||||
*/
|
||||
public static List<String> getFFMpegWebVideoCommand(String executable, String fileName) {
|
||||
List<String> command = getFFMpegGeneralFileCommand(executable, fileName);
|
||||
command.add("-vcodec");
|
||||
command.add("h264");
|
||||
command.add("-pix_fmt");
|
||||
command.add("yuv420p");
|
||||
command.add("-ar");
|
||||
command.add("48000");
|
||||
command.add("-movflags");
|
||||
command.add("+faststart");
|
||||
@NotNull
|
||||
public static FFMpegCommand getFFMpegWebVideoCommand(@NotNull String executable, @NotNull List<File> files) {
|
||||
FFMpegCommand command = getFFMpegGeneralFileCommand(executable, files);
|
||||
command.addOutputFileOption("-vcodec", "h264");
|
||||
command.addOutputFileOption("-pix_fmt", "yuv420p");
|
||||
command.addOutputFileOption("-ar", "48000");
|
||||
command.addOutputFileOption("-movflags", "+faststart");
|
||||
command.addOutputFileOption("-map_metadata", "0");
|
||||
command.addOutputFileOption("-movflags", "+use_metadata_tags");
|
||||
return command;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list containing command line arguments for a general file
|
||||
*
|
||||
* @param executable <p>The executable to use (ffmpeg/ffprobe).</p>
|
||||
* @param fileName <p>The name of the file to execute on.</p>
|
||||
* @return <p>A base list of ffmpeg commands for converting a file.</p>
|
||||
* @param executable <p>The executable to use (ffmpeg/ffprobe)</p>
|
||||
* @param files <p>The files to execute on</p>
|
||||
* @return <p>A basic FFMPEG command</p>
|
||||
*/
|
||||
public static List<String> getFFMpegGeneralFileCommand(String executable, String fileName) {
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(executable);
|
||||
command.add("-nostdin");
|
||||
command.add("-i");
|
||||
command.add(fileName);
|
||||
return command;
|
||||
@NotNull
|
||||
public static FFMpegCommand getFFMpegGeneralFileCommand(@NotNull String executable, @NotNull List<File> files) {
|
||||
FFMpegCommand command = new FFMpegCommand(executable);
|
||||
command.addGlobalOption("-nostdin");
|
||||
for (File file : files) {
|
||||
command.addInputFile(file.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds debugging parameters for only converting parts of a file
|
||||
*
|
||||
* @param command <p>The list containing the command to run.</p>
|
||||
* @param start <p>The offset before converting.</p>
|
||||
* @param length <p>The offset for stopping the conversion.</p>
|
||||
*/
|
||||
public static void addDebugArguments(List<String> command, int start, int length) {
|
||||
command.add("-ss");
|
||||
command.add("" + start);
|
||||
command.add("-t");
|
||||
command.add("" + length);
|
||||
command.addOutputFileOption("-map_metadata", "0");
|
||||
command.addOutputFileOption("-movflags", "+use_metadata_tags");
|
||||
return command;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts and prints output of a process
|
||||
*
|
||||
* @param processBuilder <p>The process to run.</p>
|
||||
* @param folder <p>The folder the process should run in.</p>
|
||||
* @param spacer <p>The character(s) to use between each new line read.</p>
|
||||
* @param write <p>Whether to write the output directly instead of storing it.</p>
|
||||
* @throws IOException <p>If the process can't be readProcess.</p>
|
||||
* @param processBuilder <p>The process to run</p>
|
||||
* @param folder <p>The folder the process should run in</p>
|
||||
* @param spacer <p>The character(s) to use between each new line read</p>
|
||||
* @param write <p>Whether to write the output directly instead of storing it</p>
|
||||
* @return <p>The result of running the process</p>
|
||||
* @throws IOException <p>If the process can't be readProcess</p>
|
||||
*/
|
||||
public static String runProcess(ProcessBuilder processBuilder, File folder, String spacer, boolean write)
|
||||
throws IOException {
|
||||
@NotNull
|
||||
public static ProcessResult runProcess(@NotNull ProcessBuilder processBuilder, @Nullable File folder,
|
||||
@NotNull String spacer, boolean write) throws IOException {
|
||||
//Give the user information about what's about to happen
|
||||
OutputUtil.print("Command to be run: ");
|
||||
OutputUtil.println(processBuilder.command().toString());
|
||||
|
||||
//Set directory and error stream
|
||||
if (folder != null) {
|
||||
processBuilder.directory(folder);
|
||||
}
|
||||
processBuilder.redirectErrorStream(true);
|
||||
|
||||
Process process = processBuilder.start();
|
||||
@ -109,7 +116,10 @@ public final class FFMpegHelper {
|
||||
StringBuilder output = new StringBuilder();
|
||||
while (process.isAlive()) {
|
||||
String read = readProcess(processReader, spacer);
|
||||
if (!read.equals("")) {
|
||||
if (read.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (write) {
|
||||
OutputUtil.println(read);
|
||||
} else {
|
||||
@ -117,82 +127,64 @@ public final class FFMpegHelper {
|
||||
output.append(read);
|
||||
}
|
||||
}
|
||||
}
|
||||
OutputUtil.println("Process finished.");
|
||||
return output.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds audio to a command
|
||||
*
|
||||
* @param command <p>The command to add audio to.</p>
|
||||
* @param audioStream <p>The audio stream to be added.</p>
|
||||
* @param toStereo <p>Whether to convert the audio stream to stereo.</p>
|
||||
*/
|
||||
public static void addAudioStream(List<String> command, AudioStream audioStream, boolean toStereo) {
|
||||
if (audioStream != null) {
|
||||
command.add("-map");
|
||||
command.add("0:" + audioStream.getAbsoluteIndex());
|
||||
if (toStereo && audioStream.getChannels() > 2) {
|
||||
command.add("-af");
|
||||
command.add("pan=stereo|FL=FC+0.30*FL+0.30*BL|FR=FC+0.30*FR+0.30*BR");
|
||||
//command.add("pan=stereo|FL < 1.0*FL + 0.707*FC + 0.707*BL|FR < 1.0*FR + 0.707*FC + 0.707*BR");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds subtitles and video mapping to a command
|
||||
*
|
||||
* @param command <p>The list containing the rest of the command.</p>
|
||||
* @param subtitleStream <p>The subtitle stream to be used.</p>
|
||||
* @param videoStream <p>The video stream to be used.</p>
|
||||
* @param file <p>The file to convert.</p>
|
||||
*/
|
||||
public static void addSubtitleAndVideoStream(List<String> command, SubtitleStream subtitleStream,
|
||||
VideoStream videoStream, File file) {
|
||||
//No appropriate subtitle was found. Just add the video stream.
|
||||
if (subtitleStream == null) {
|
||||
addVideoStream(command, videoStream);
|
||||
return;
|
||||
}
|
||||
|
||||
//Add the correct command arguments depending on the subtitle type
|
||||
if (!subtitleStream.getIsImageSubtitle()) {
|
||||
addSubtitle(command, subtitleStream, videoStream);
|
||||
} else if (file.getName().equals(subtitleStream.getFile())) {
|
||||
addInternalImageSubtitle(command, subtitleStream, videoStream);
|
||||
} else {
|
||||
addExternalImageSubtitle(command, subtitleStream, videoStream);
|
||||
try {
|
||||
int exitCode = process.waitFor();
|
||||
OutputUtil.println("Process finished with exit code: " + exitCode);
|
||||
return new ProcessResult(exitCode, output.toString());
|
||||
} catch (InterruptedException e) {
|
||||
return new ProcessResult(1, output.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds video mapping to a command
|
||||
* Adds arguments for converting a file to h264 using hardware acceleration
|
||||
*
|
||||
* @param command <p>The list containing the rest of the command.</p>
|
||||
* @param videoStream <p>The video stream to be used.</p>
|
||||
* @param command <p>The command to add the arguments to</p>
|
||||
* @param quality <p>The quality to encode. 0 = best, 51 = worst.</p>
|
||||
*/
|
||||
private static void addVideoStream(List<String> command, VideoStream videoStream) {
|
||||
command.add("-map");
|
||||
command.add(String.format("0:%d", videoStream.getAbsoluteIndex()));
|
||||
public static void addH264HardwareEncoding(@NotNull FFMpegCommand command, int quality) {
|
||||
command.addOutputFileOption("-codec:v", "h264_nvenc");
|
||||
command.addOutputFileOption("-profile", "high");
|
||||
command.addOutputFileOption("-preset", "p7");
|
||||
command.addOutputFileOption("-crf", String.valueOf(quality));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds subtitle commands to a command list
|
||||
* Adds arguments for converting a file to h265 using hardware acceleration
|
||||
*
|
||||
* @param command <p>The list containing the FFmpeg commands.</p>
|
||||
* @param subtitleStream <p>The subtitle stream to add.</p>
|
||||
* @param videoStream <p>The video stream to burn the subtitle into.</p>
|
||||
* @param command <p>The command to add the arguments to</p>
|
||||
* @param quality <p>The quality to encode. 0 = best, 51 = worst.</p>
|
||||
*/
|
||||
private static void addSubtitle(List<String> command, SubtitleStream subtitleStream, VideoStream videoStream) {
|
||||
command.add("-map");
|
||||
command.add(String.format("0:%d", videoStream.getAbsoluteIndex()));
|
||||
command.add("-vf");
|
||||
String safeFileName = escapeSpecialCharactersInFileName(subtitleStream.getFile());
|
||||
String subtitleCommand = String.format("subtitles='%s':si=%d", safeFileName,
|
||||
subtitleStream.getRelativeIndex());
|
||||
command.add(subtitleCommand);
|
||||
public static void addH265HardwareEncoding(@NotNull FFMpegCommand command, int quality) {
|
||||
command.addOutputFileOption("-codec:v", "hevc_nvenc");
|
||||
command.addOutputFileOption("-profile", "main10");
|
||||
command.addOutputFileOption("-preset", "p7");
|
||||
command.addOutputFileOption("-tag:v", "hvc1");
|
||||
command.addOutputFileOption("-crf", String.valueOf(quality));
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps all streams in the given list to the output in the given command
|
||||
*
|
||||
* @param command <p>The command to add the mappings to</p>
|
||||
* @param streams <p>The streams to map</p>
|
||||
* @param <K> <p>The type of stream object to map</p>
|
||||
*/
|
||||
public static <K extends StreamObject> void mapAllStreams(@NotNull FFMpegCommand command, @NotNull List<K> streams) {
|
||||
for (StreamObject stream : streams) {
|
||||
mapStream(command, stream);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the given stream to the given FFMPEG command's output
|
||||
*
|
||||
* @param command <p>The command to map the stream to</p>
|
||||
* @param stream <p>The stream to map</p>
|
||||
*/
|
||||
public static void mapStream(@NotNull FFMpegCommand command, @NotNull StreamObject stream) {
|
||||
command.addOutputFileOption("-map", String.format("%d:%d", stream.getInputIndex(),
|
||||
stream.getAbsoluteIndex()));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -201,7 +193,8 @@ public final class FFMpegHelper {
|
||||
* @param fileName <p>The filename to escape.</p>
|
||||
* @return <p>A filename with known special characters escaped.</p>
|
||||
*/
|
||||
private static String escapeSpecialCharactersInFileName(String fileName) {
|
||||
@NotNull
|
||||
public static String escapeSpecialCharactersInFileName(@NotNull String fileName) {
|
||||
return fileName.replaceAll("\\\\", "\\\\\\\\\\\\\\\\")
|
||||
.replaceAll("'", "'\\\\\\\\\\\\\''")
|
||||
.replaceAll("%", "\\\\\\\\\\\\%")
|
||||
@ -211,37 +204,23 @@ public final class FFMpegHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds image subtitle commands to a command list
|
||||
* Gets the nth stream from a list of streams
|
||||
*
|
||||
* @param command <p>The list containing the FFmpeg commands.</p>
|
||||
* @param subtitleStream <p>The subtitle stream to add.</p>
|
||||
* @param videoStream <p>The video stream to burn the subtitle into.</p>
|
||||
* @param streams <p>A list of streams</p>
|
||||
* @param n <p>The index of the audio stream to get</p>
|
||||
* @return <p>The first audio stream found, or null if no audio streams were found</p>
|
||||
*/
|
||||
private static void addInternalImageSubtitle(List<String> command, SubtitleStream subtitleStream,
|
||||
VideoStream videoStream) {
|
||||
command.add("-filter_complex");
|
||||
String filter = String.format("[0:v:%d][0:%d]overlay", videoStream.getAbsoluteIndex(),
|
||||
subtitleStream.getAbsoluteIndex());
|
||||
command.add(filter);
|
||||
public static <G extends StreamObject> G getNthSteam(@NotNull List<G> streams, int n) {
|
||||
if (n < 0) {
|
||||
throw new IllegalArgumentException("N cannot be negative!");
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds external image subtitle commands to a command list
|
||||
*
|
||||
* @param command <p>The list containing the FFmpeg commands.</p>
|
||||
* @param externalImageSubtitle <p>The external image subtitle stream to add.</p>
|
||||
* @param videoStream <p>The video stream to burn the subtitle into.</p>
|
||||
*/
|
||||
private static void addExternalImageSubtitle(List<String> command, SubtitleStream externalImageSubtitle,
|
||||
VideoStream videoStream) {
|
||||
command.add("-i");
|
||||
command.add(externalImageSubtitle.getFile());
|
||||
command.add("-filter_complex");
|
||||
command.add(String.format("[1:s]scale=width=%d:height=%d,crop=w=%d:h=%d:x=0:y=out_h[sub];[%d:v]" +
|
||||
"[sub]overlay", videoStream.getWidth(), videoStream.getHeight(), videoStream.getWidth(),
|
||||
videoStream.getHeight(), videoStream.getAbsoluteIndex()));
|
||||
command.add("-profile:v");
|
||||
command.add("baseline");
|
||||
G stream = null;
|
||||
if (streams.size() > n) {
|
||||
stream = streams.get(n);
|
||||
} else if (!streams.isEmpty()) {
|
||||
stream = streams.get(0);
|
||||
}
|
||||
return stream;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -252,80 +231,200 @@ public final class FFMpegHelper {
|
||||
* @return <p>A list of streams.</p>
|
||||
* @throws IOException <p>If something goes wrong while probing.</p>
|
||||
*/
|
||||
private static String[] probeForStreams(String ffprobePath, File file) throws IOException {
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(
|
||||
ffprobePath,
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"stream_tags=language,title:stream=index,codec_name,codec_type,channels,codec_type,width,height",
|
||||
file.toString()
|
||||
);
|
||||
String result = runProcess(processBuilder, file.getParentFile(), PROBE_SPLIT_CHARACTER, false);
|
||||
return StringUtil.stringBetween(result, "[STREAM]", "[/STREAM]");
|
||||
@NotNull
|
||||
private static List<String> probeForStreams(@NotNull String ffprobePath, @NotNull File file) throws IOException {
|
||||
FFMpegCommand probeCommand = new FFMpegCommand(ffprobePath);
|
||||
probeCommand.addGlobalOption("-v", "error", "-show_streams");
|
||||
probeCommand.addInputFile(file.toString());
|
||||
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(probeCommand.getResult());
|
||||
ProcessResult result = runProcess(processBuilder, file.getParentFile(), PROBE_SPLIT_CHARACTER, false);
|
||||
if (result.exitCode() != 0) {
|
||||
throw new IllegalArgumentException("File probe failed with code " + result.exitCode());
|
||||
}
|
||||
return StringUtil.stringBetween(result.output(), "[STREAM]", "[/STREAM]");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the duration, in seconds, of the given file
|
||||
*
|
||||
* @param ffprobePath <p>The path to the ffprobe executable</p>
|
||||
* @param file <p>The file to get the duration of</p>
|
||||
* @return <p>The duration</p>
|
||||
* @throws IOException <p>If unable to probe the file</p>
|
||||
* @throws NumberFormatException <p>If ffmpeg returns a non-number</p>
|
||||
*/
|
||||
public static double getDuration(@NotNull String ffprobePath, @NotNull File file) throws IOException, NumberFormatException {
|
||||
FFMpegCommand probeCommand = new FFMpegCommand(ffprobePath);
|
||||
probeCommand.addGlobalOption("-v", "error", "-show_entries", "format=duration", "-of",
|
||||
"default=noprint_wrappers=1:nokey=1");
|
||||
probeCommand.addInputFile(file.toString());
|
||||
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(probeCommand.getResult());
|
||||
ProcessResult result = runProcess(processBuilder, file.getParentFile(), "", false);
|
||||
if (result.exitCode() != 0) {
|
||||
throw new IllegalArgumentException("File probe failed with code " + result.exitCode());
|
||||
}
|
||||
return Double.parseDouble(result.output().trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a list of all streams and parses each stream into one of three objects
|
||||
*
|
||||
* @param ffprobePath <p>The path to the ffprobe executable</p>
|
||||
* @param streams <p>A list of all streams for the current file.</p>
|
||||
* @param file <p>The file currently being converted.</p>
|
||||
* @param subtitleFormats <p>The extensions to accept for external subtitles</p>
|
||||
* @return <p>A list of StreamObjects.</p>
|
||||
*/
|
||||
private static List<StreamObject> parseStreams(String ffprobePath, String[] streams, File file) throws IOException {
|
||||
@NotNull
|
||||
private static StreamProbeResult parseStreams(@NotNull String ffprobePath, @NotNull List<String> streams,
|
||||
@NotNull File file, @NotNull List<String> subtitleFormats) throws IOException {
|
||||
StreamProbeResult probeResult = new StreamProbeResult(new ArrayList<>(List.of(file)),
|
||||
parseStreamObjects(streams));
|
||||
getExternalStreams(probeResult, ffprobePath, file.getParentFile(), file.getName(), subtitleFormats);
|
||||
return probeResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the stream objects found in the given streams
|
||||
*
|
||||
* @param streams <p>The stream data to parse</p>
|
||||
* @return <p>The parsed stream objects</p>
|
||||
*/
|
||||
@NotNull
|
||||
private static List<StreamObject> parseStreamObjects(@NotNull List<String> streams) {
|
||||
List<StreamObject> parsedStreams = new ArrayList<>();
|
||||
int relativeAudioIndex = 0;
|
||||
int relativeVideoIndex = 0;
|
||||
int relativeSubtitleIndex = 0;
|
||||
|
||||
for (String stream : streams) {
|
||||
String[] streamParts = stream.split(PROBE_SPLIT_CHARACTER);
|
||||
if (stream.contains("codec_type=video")) {
|
||||
parsedStreams.add(parseVideoStream(streamParts, relativeVideoIndex++));
|
||||
} else if (stream.contains("codec_type=audio")) {
|
||||
parsedStreams.add(parseAudioStream(streamParts, relativeAudioIndex++));
|
||||
} else if (stream.contains("codec_type=subtitle")) {
|
||||
parsedStreams.add(parseSubtitleStream(streamParts, relativeSubtitleIndex++, file.getName()));
|
||||
Map<StreamTag, String> streamInfo = getStreamInfo(streamParts);
|
||||
StreamType streamType = getStreamType(streamInfo);
|
||||
|
||||
switch (streamType) {
|
||||
case VIDEO -> parsedStreams.add(new VideoStream(streamInfo, 0, relativeVideoIndex++));
|
||||
case AUDIO -> parsedStreams.add(new AudioStream(streamInfo, 0, relativeAudioIndex++));
|
||||
case SUBTITLE -> parsedStreams.add(new SubtitleStream(streamInfo, 0, relativeSubtitleIndex++));
|
||||
case OTHER -> parsedStreams.add(new OtherStream(streamInfo, 0, false));
|
||||
case COVER_IMAGE -> {
|
||||
if (FFMpegConvert.getConfiguration().copyAttachedImages()) {
|
||||
parsedStreams.add(new OtherStream(streamInfo, 0, true));
|
||||
}
|
||||
}
|
||||
case DATA -> OutputUtil.print("A binary stream was found. Those are ignored they will generally " +
|
||||
"cause the conversion to fail.");
|
||||
}
|
||||
}
|
||||
List<StreamObject> externalSubtitles = getExternalSubtitles(ffprobePath, file.getParentFile(), file.getName());
|
||||
parsedStreams.addAll(externalSubtitles);
|
||||
return parsedStreams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether there exists an external image subtitle with the same filename as the file
|
||||
* Gets the type of a stream from its stream info
|
||||
*
|
||||
* @param ffprobePath <p>The path/command to ffprobe.</p>
|
||||
* @param directory <p>The directory containing the file.</p>
|
||||
* @param convertingFile <p>The file to be converted.</p>
|
||||
* @return <p>The extension of the subtitle or empty if no subtitle was found.</p>
|
||||
* @param streamInfo <p>The information describing the stream</p>
|
||||
* @return <p>The type of the stream</p>
|
||||
*/
|
||||
private static List<StreamObject> getExternalSubtitles(String ffprobePath, File directory, String convertingFile)
|
||||
throws IOException {
|
||||
List<StreamObject> parsedStreams = new ArrayList<>();
|
||||
@NotNull
|
||||
private static StreamType getStreamType(@NotNull Map<StreamTag, String> streamInfo) {
|
||||
String codecType = ValueParsingHelper.parseString(streamInfo.get(StreamTag.CODEC_TYPE), "");
|
||||
switch (codecType) {
|
||||
case "video":
|
||||
String mime = ValueParsingHelper.parseString(streamInfo.get(StreamTag.TAG_MIME_TYPE), "");
|
||||
// Some attached covers are marked as video streams
|
||||
if (ValueParsingHelper.parseInt(streamInfo.get(StreamTag.DISPOSITION_ATTACHED_PIC), 0) != 1 &&
|
||||
!mime.startsWith("image/") && !mime.endsWith("-font")) {
|
||||
return StreamType.VIDEO;
|
||||
} else {
|
||||
return StreamType.COVER_IMAGE;
|
||||
}
|
||||
case "audio":
|
||||
return StreamType.AUDIO;
|
||||
case "subtitle":
|
||||
return StreamType.SUBTITLE;
|
||||
case "data":
|
||||
return StreamType.DATA;
|
||||
default:
|
||||
return StreamType.OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets stream info from the given raw stream info lines
|
||||
*
|
||||
* @param streamParts <p>The stream info lines to parse</p>
|
||||
* @return <p>The stream tag map parsed</p>
|
||||
*/
|
||||
@NotNull
|
||||
private static Map<StreamTag, String> getStreamInfo(@Nullable String[] streamParts) {
|
||||
Map<StreamTag, String> streamInfo = new HashMap<>();
|
||||
for (String part : streamParts) {
|
||||
if (part == null || !part.contains("=")) {
|
||||
continue;
|
||||
}
|
||||
String[] keyValue = part.split("=");
|
||||
String value = keyValue.length > 1 ? keyValue[1] : "";
|
||||
|
||||
StreamTag tag = StreamTag.getFromString(keyValue[0]);
|
||||
if (tag != null) {
|
||||
streamInfo.put(tag, value);
|
||||
}
|
||||
}
|
||||
return streamInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find any external files adjacent to the first input file, and appends it to the given probe result
|
||||
*
|
||||
* @param streamProbeResult <p>The stream probe result to append to</p>
|
||||
* @param ffprobePath <p>The path/command to ffprobe</p>
|
||||
* @param directory <p>The directory containing the file</p>
|
||||
* @param convertingFile <p>The first/main file to be converted</p>
|
||||
* @param formats <p>The extensions to accept for external tracks</p>
|
||||
*/
|
||||
private static void getExternalStreams(@NotNull StreamProbeResult streamProbeResult,
|
||||
@NotNull String ffprobePath, @NotNull File directory,
|
||||
@NotNull String convertingFile,
|
||||
@NotNull List<String> formats) throws IOException {
|
||||
//Find all files in the same directory with external subtitle formats
|
||||
String[] subtitleFormats = FileUtil.readFileLines("subtitle_formats.txt");
|
||||
File[] subtitleFiles = FileUtil.listFilesRecursive(directory, subtitleFormats, 1);
|
||||
File[] files = FileUtil.listFilesRecursive(directory, formats, 1);
|
||||
|
||||
//Return early if no files were found
|
||||
if (subtitleFiles == null) {
|
||||
return parsedStreams;
|
||||
if (files == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String fileTitle = FileUtil.stripExtension(convertingFile);
|
||||
List<File> subtitleFilesList = new ArrayList<>(Arrays.asList(subtitleFiles));
|
||||
List<File> filesList = new ArrayList<>(Arrays.asList(files));
|
||||
|
||||
//Finds the files which are subtitles probably belonging to the file
|
||||
subtitleFilesList = ListUtil.getMatching(subtitleFilesList,
|
||||
(subtitleFile) -> subtitleFile.getName().contains(fileTitle));
|
||||
for (File subtitleFile : subtitleFilesList) {
|
||||
filesList = ListUtil.getMatching(filesList, (file) -> file.getName().contains(fileTitle));
|
||||
|
||||
for (File file : filesList) {
|
||||
int inputIndex = streamProbeResult.parsedFiles().size();
|
||||
streamProbeResult.parsedFiles().add(file);
|
||||
//Probe the files and add them to the result list
|
||||
String[] streams = probeForStreams(ffprobePath, subtitleFile);
|
||||
List<String> streams = probeForStreams(ffprobePath, file);
|
||||
|
||||
int audioIndex = 0;
|
||||
int subtitleIndex = 0;
|
||||
int videoIndex = 0;
|
||||
for (String stream : streams) {
|
||||
String[] streamParts = stream.split(PROBE_SPLIT_CHARACTER);
|
||||
parsedStreams.add(parseSubtitleStream(streamParts, 0, subtitleFile.getName()));
|
||||
Map<StreamTag, String> streamInfo = getStreamInfo(streamParts);
|
||||
StreamObject streamObject = null;
|
||||
switch (getStreamType(streamInfo)) {
|
||||
case SUBTITLE -> streamObject = new SubtitleStream(streamInfo, inputIndex, subtitleIndex++);
|
||||
case AUDIO -> streamObject = new AudioStream(streamInfo, inputIndex, audioIndex++);
|
||||
case VIDEO -> streamObject = new VideoStream(streamInfo, inputIndex, videoIndex++);
|
||||
}
|
||||
if (streamObject != null) {
|
||||
streamProbeResult.parsedStreams().add(streamObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
return parsedStreams;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -335,98 +434,31 @@ public final class FFMpegHelper {
|
||||
* @return <p>The output from the readProcess.</p>
|
||||
* @throws IOException <p>On reader failure.</p>
|
||||
*/
|
||||
private static String readProcess(BufferedReader reader, String spacer) throws IOException {
|
||||
@NotNull
|
||||
private static String readProcess(@NotNull BufferedReader reader, @NotNull String spacer) throws IOException {
|
||||
String line;
|
||||
StringBuilder text = new StringBuilder();
|
||||
while (reader.ready() && (line = reader.readLine()) != null && !line.equals("") && !line.equals("\n")) {
|
||||
while (reader.ready() && (line = reader.readLine()) != null && !line.isEmpty() && !line.equals("\n")) {
|
||||
text.append(line).append(spacer);
|
||||
}
|
||||
return text.toString().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a list of video stream parameters to a video stream object
|
||||
* Gets available hardware acceleration types
|
||||
*
|
||||
* @param streamParts <p>A list of parameters belonging to an video stream.</p>
|
||||
* @param relativeIndex <p>The relative index of the video stream.</p>
|
||||
* @return <p>A SubtitleStream object.</p>
|
||||
* @throws NumberFormatException <p>If codec index contains a non-numeric value.</p>
|
||||
* @param ffmpegPath <p>The path to ffmpeg's executable</p>
|
||||
* @return <p>The available hardware acceleration methods</p>
|
||||
* @throws IOException <p>If the process fails</p>
|
||||
*/
|
||||
private static VideoStream parseVideoStream(String[] streamParts, int relativeIndex) throws NumberFormatException {
|
||||
String codec = null;
|
||||
int absoluteIndex = -1;
|
||||
int width = -1;
|
||||
int height = -1;
|
||||
for (String streamPart : streamParts) {
|
||||
if (streamPart.startsWith("codec_name=")) {
|
||||
codec = streamPart.replace("codec_name=", "");
|
||||
} else if (streamPart.startsWith("index=")) {
|
||||
absoluteIndex = Integer.parseInt(streamPart.replace("index=", ""));
|
||||
} else if (streamPart.startsWith("width=")) {
|
||||
width = Integer.parseInt(streamPart.replace("width=", ""));
|
||||
} else if (streamPart.startsWith("height=")) {
|
||||
height = Integer.parseInt(streamPart.replace("height=", ""));
|
||||
}
|
||||
}
|
||||
return new VideoStream(codec, absoluteIndex, relativeIndex, width, height);
|
||||
@NotNull
|
||||
public static List<String> getHWAcceleration(@NotNull String ffmpegPath) throws IOException {
|
||||
FFMpegCommand probeCommand = new FFMpegCommand(ffmpegPath);
|
||||
probeCommand.addGlobalOption("-v", "error", "-hwaccels");
|
||||
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(probeCommand.getResult());
|
||||
ProcessResult result = runProcess(processBuilder, null, PROBE_SPLIT_CHARACTER, false);
|
||||
return List.of(result.output().split(PROBE_SPLIT_CHARACTER));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a list of audio stream parameters to an audio stream object
|
||||
*
|
||||
* @param streamParts <p>A list of parameters belonging to an audio stream.</p>
|
||||
* @param relativeIndex <p>The relative index of the audio stream.</p>
|
||||
* @return <p>A SubtitleStream object.</p>
|
||||
* @throws NumberFormatException <p>If codec index contains a non-numeric value.</p>
|
||||
*/
|
||||
private static AudioStream parseAudioStream(String[] streamParts, int relativeIndex) throws NumberFormatException {
|
||||
String codec = null;
|
||||
int absoluteIndex = -1;
|
||||
String language = null;
|
||||
int channels = 0;
|
||||
String title = "";
|
||||
for (String streamPart : streamParts) {
|
||||
if (streamPart.startsWith("codec_name=")) {
|
||||
codec = streamPart.replace("codec_name=", "");
|
||||
} else if (streamPart.startsWith("index=")) {
|
||||
absoluteIndex = Integer.parseInt(streamPart.replace("index=", ""));
|
||||
} else if (streamPart.startsWith("TAG:language=")) {
|
||||
language = streamPart.replace("TAG:language=", "");
|
||||
} else if (streamPart.startsWith("channels=")) {
|
||||
channels = Integer.parseInt(streamPart.replace("channels=", ""));
|
||||
} else if (streamPart.startsWith("TAG:title=")) {
|
||||
title = streamPart.replace("TAG:title=", "");
|
||||
}
|
||||
}
|
||||
return new AudioStream(codec, absoluteIndex, relativeIndex, language, title, channels);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a list of subtitle stream parameters to a subtitle stream object
|
||||
*
|
||||
* @param streamParts <p>A list of parameters belonging to a subtitle stream.</p>
|
||||
* @param relativeIndex <p>The relative index of the subtitle.</p>
|
||||
* @param file <p>The file currently being converted.</p>
|
||||
* @return <p>A SubtitleStream object.</p>
|
||||
* @throws NumberFormatException <p>If codec index contains a non-numeric value.</p>
|
||||
*/
|
||||
private static SubtitleStream parseSubtitleStream(String[] streamParts, int relativeIndex, String file)
|
||||
throws NumberFormatException {
|
||||
String codecName = null;
|
||||
int absoluteIndex = -1;
|
||||
String language = null;
|
||||
String title = "";
|
||||
for (String streamPart : streamParts) {
|
||||
if (streamPart.startsWith("codec_name=")) {
|
||||
codecName = streamPart.replace("codec_name=", "");
|
||||
} else if (streamPart.startsWith("index=")) {
|
||||
absoluteIndex = Integer.parseInt(streamPart.replace("index=", ""));
|
||||
} else if (streamPart.startsWith("TAG:language=")) {
|
||||
language = streamPart.replace("TAG:language=", "");
|
||||
} else if (streamPart.startsWith("TAG:title=")) {
|
||||
title = streamPart.replace("TAG:title=", "");
|
||||
}
|
||||
}
|
||||
return new SubtitleStream(codecName, absoluteIndex, relativeIndex, language, title, file);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,178 @@
|
||||
package net.knarcraft.ffmpegconverter.utility;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A helper class for dealing with files
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public final class FileHelper {
|
||||
|
||||
private FileHelper() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a buffered reader for an internal file
|
||||
*
|
||||
* @param file <p>The name of the file to get a buffered reader for (start with a '/'). The file should reside in
|
||||
* the resources directory.</p>
|
||||
* @return <p>A buffered read for reading the file</p>
|
||||
* @throws FileNotFoundException <p>If unable to get an input stream for the given file</p>
|
||||
*/
|
||||
@NotNull
|
||||
public static BufferedReader getBufferedReaderForInternalFile(@NotNull String file) throws FileNotFoundException {
|
||||
InputStream inputStream = getInputStreamForInternalFile(file);
|
||||
if (inputStream == null) {
|
||||
throw new FileNotFoundException("Unable to read the given file");
|
||||
}
|
||||
return getBufferedReaderFromInputStream(inputStream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an input stream from a string pointing to an internal file
|
||||
*
|
||||
* <p>This is used for getting an input stream for reading a file contained within the compiled .jar file. The file
|
||||
* should be in the resources directory, and the file path should start with a forward slash ("/") character.</p>
|
||||
*
|
||||
* @param file <p>The file to read</p>
|
||||
* @return <p>An input stream for the file</p>
|
||||
*/
|
||||
@Nullable
|
||||
public static InputStream getInputStreamForInternalFile(@NotNull String file) {
|
||||
return FileHelper.class.getResourceAsStream(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a buffered reader from a string pointing to a file
|
||||
*
|
||||
* @param file <p>The file to read</p>
|
||||
* @return <p>A buffered reader reading the file</p>
|
||||
* @throws FileNotFoundException <p>If the given file does not exist</p>
|
||||
*/
|
||||
@NotNull
|
||||
|
||||
public static BufferedReader getBufferedReaderFromString(@NotNull String file) throws FileNotFoundException {
|
||||
FileInputStream fileInputStream = new FileInputStream(file);
|
||||
return getBufferedReaderFromInputStream(fileInputStream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a buffered reader given an input stream
|
||||
*
|
||||
* @param inputStream <p>The input stream to read</p>
|
||||
* @return <p>A buffered reader reading the input stream</p>
|
||||
*/
|
||||
@NotNull
|
||||
public static BufferedReader getBufferedReaderFromInputStream(@NotNull InputStream inputStream) {
|
||||
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
|
||||
return new BufferedReader(inputStreamReader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a buffered writer from a string pointing to a file
|
||||
*
|
||||
* @param file <p>The file to write to</p>
|
||||
* @return <p>A buffered writer writing to the file</p>
|
||||
* @throws FileNotFoundException <p>If the file does not exist</p>
|
||||
*/
|
||||
@NotNull
|
||||
public static BufferedWriter getBufferedWriterFromString(@NotNull String file) throws FileNotFoundException {
|
||||
FileOutputStream fileOutputStream = new FileOutputStream(file);
|
||||
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, StandardCharsets.UTF_8);
|
||||
return new BufferedWriter(outputStreamWriter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads key/value pairs from a buffered reader
|
||||
*
|
||||
* @param bufferedReader <p>The buffered reader to read</p>
|
||||
* @param separator <p>The separator separating a key from a value</p>
|
||||
* @return <p>A map containing the read pairs</p>
|
||||
* @throws IOException <p>If unable to read from the stream</p>
|
||||
*/
|
||||
@NotNull
|
||||
public static Map<String, String> readKeyValuePairs(@NotNull BufferedReader bufferedReader,
|
||||
@NotNull String separator) throws IOException {
|
||||
Map<String, String> readPairs = new HashMap<>();
|
||||
List<String> lines = readLines(bufferedReader);
|
||||
|
||||
for (String line : lines) {
|
||||
int separatorIndex = line.indexOf(separator);
|
||||
if (separatorIndex == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//Read the line
|
||||
String key = line.substring(0, separatorIndex);
|
||||
String value = line.substring(separatorIndex + 1);
|
||||
readPairs.put(key, value);
|
||||
}
|
||||
|
||||
return readPairs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a list from a buffered reader
|
||||
*
|
||||
* @param bufferedReader <p>The buffered reader to read</p>
|
||||
* @return <p>A list of the read strings</p>
|
||||
* @throws IOException <p>If unable to read from the stream</p>
|
||||
*/
|
||||
@NotNull
|
||||
public static List<String> readLines(@NotNull BufferedReader bufferedReader) throws IOException {
|
||||
List<String> readLines = new ArrayList<>();
|
||||
|
||||
String line = bufferedReader.readLine();
|
||||
boolean firstLine = true;
|
||||
while (line != null) {
|
||||
//Strip UTF BOM from the first line
|
||||
if (firstLine) {
|
||||
line = removeUTF8BOM(line);
|
||||
firstLine = false;
|
||||
}
|
||||
//Split at first separator
|
||||
if (line.isEmpty()) {
|
||||
line = bufferedReader.readLine();
|
||||
continue;
|
||||
}
|
||||
|
||||
readLines.add(line);
|
||||
line = bufferedReader.readLine();
|
||||
}
|
||||
bufferedReader.close();
|
||||
|
||||
return readLines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the UTF-8 Byte Order Mark if present
|
||||
*
|
||||
* @param string <p>The string to remove the BOM from</p>
|
||||
* @return <p>A string guaranteed without a BOM</p>
|
||||
*/
|
||||
private static @NotNull String removeUTF8BOM(@NotNull String string) {
|
||||
String UTF8_BOM = "\uFEFF";
|
||||
if (string.startsWith(UTF8_BOM)) {
|
||||
string = string.substring(1);
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
package net.knarcraft.ffmpegconverter.utility;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A class which helps with file handling
|
||||
@ -12,6 +12,7 @@ import java.io.InputStreamReader;
|
||||
public final class FileUtil {
|
||||
|
||||
private FileUtil() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -22,19 +23,10 @@ public final class FileUtil {
|
||||
* @param outExtension <p>The extension of the output file.</p>
|
||||
* @return <p>A file name with the new extension and without any collisions.</p>
|
||||
*/
|
||||
public static String getNonCollidingPath(File folder, File file, String outExtension) {
|
||||
@NotNull
|
||||
public static String getNonCollidingPath(@NotNull File folder, @NotNull File file, @NotNull String outExtension) {
|
||||
return FileUtil.getNonCollidingFilename(folder.getAbsolutePath() + File.separator +
|
||||
FileUtil.stripExtension(file) + "." + outExtension, outExtension);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the extension from a file name
|
||||
*
|
||||
* @param file <p>A filename.</p>
|
||||
* @return <p>A filename without its extension.</p>
|
||||
*/
|
||||
static String stripExtension(String file) {
|
||||
return file.substring(0, file.lastIndexOf('.'));
|
||||
FileUtil.stripExtension(file.getName()) + "." + outExtension, outExtension);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -44,14 +36,16 @@ public final class FileUtil {
|
||||
* @param maxRecursions <p>Maximum number of recursions</p>
|
||||
* @return A list of files
|
||||
*/
|
||||
public static File[] listFilesRecursive(File folder, String[] extensions, int maxRecursions) {
|
||||
@Nullable
|
||||
public static File[] listFilesRecursive(@NotNull File folder, @NotNull List<String> extensions, int maxRecursions) {
|
||||
//Return if the target depth has been reached
|
||||
if (maxRecursions == 0) {
|
||||
return null;
|
||||
}
|
||||
//Get a list of all files which are folders and has one of the extensions specified
|
||||
File[] foundFiles = folder.listFiles((file) -> file.isFile() &&
|
||||
ListUtil.listContains(extensions, (item) -> file.getName().endsWith(item)));
|
||||
ListUtil.listContains(extensions, (item) -> file.getName().toLowerCase().endsWith(item)));
|
||||
|
||||
//Return if recursion is finished
|
||||
if (maxRecursions == 1) {
|
||||
return foundFiles;
|
||||
@ -77,36 +71,6 @@ public final class FileUtil {
|
||||
return foundFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a file's contents to a string list
|
||||
*
|
||||
* <p>The file must contain the number of lines to read in the first line.</p>
|
||||
*
|
||||
* @param fileName <p>The file to read.</p>
|
||||
* @return <p>A string list where each element is one line of the file.</p>
|
||||
* @throws IOException <p>If the file cannot be read.</p>
|
||||
*/
|
||||
public static String[] readFileLines(String fileName) throws IOException {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(getResourceAsStream(fileName)));
|
||||
int numberOfLines = Integer.parseInt(reader.readLine());
|
||||
String[] lines = new String[numberOfLines];
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
lines[i] = reader.readLine();
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a resource as an InputStream
|
||||
*
|
||||
* @param resourceName <p>The name of the resource you want to read.</p>
|
||||
* @return <p>An input stream which can be used to access the resource.</p>
|
||||
*/
|
||||
private static InputStream getResourceAsStream(String resourceName) {
|
||||
ClassLoader classloader = Thread.currentThread().getContextClassLoader();
|
||||
return classloader.getResourceAsStream(resourceName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds parentheses with an integer if the output file already exists
|
||||
*
|
||||
@ -114,9 +78,10 @@ public final class FileUtil {
|
||||
* @param extension <p>The extension of the target file.</p>
|
||||
* @return <p>A filename guaranteed not to collide with other files.</p>
|
||||
*/
|
||||
private static String getNonCollidingFilename(String targetPath, String extension) {
|
||||
@NotNull
|
||||
private static String getNonCollidingFilename(@NotNull String targetPath, @NotNull String extension) {
|
||||
File newFile = new File(targetPath);
|
||||
String fileName = stripExtension(targetPath);
|
||||
String fileName = stripExtension(targetPath).replaceAll("\\([0-9]+\\)$", "");
|
||||
int i = 1;
|
||||
while (newFile.exists()) {
|
||||
newFile = new File(fileName + "(" + i++ + ")" + "." + extension);
|
||||
@ -125,13 +90,29 @@ public final class FileUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets filename without extension from File object
|
||||
* Gets the extension of the given filename
|
||||
*
|
||||
* @param file <p>A file object.</p>
|
||||
* @return <p>A filename.</p>
|
||||
* @param file <p>The filename to check</p>
|
||||
* @return <p>The file's extension</p>
|
||||
*/
|
||||
private static String stripExtension(File file) {
|
||||
return file.getName().substring(0, file.getName().lastIndexOf('.'));
|
||||
@NotNull
|
||||
public static String getExtension(@NotNull String file) {
|
||||
if (file.contains(".")) {
|
||||
return file.substring(file.lastIndexOf('.') + 1);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the extension from a file name
|
||||
*
|
||||
* @param file <p>A filename.</p>
|
||||
* @return <p>A filename without its extension.</p>
|
||||
*/
|
||||
@NotNull
|
||||
public static String stripExtension(@NotNull String file) {
|
||||
return file.substring(0, file.lastIndexOf('.'));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package net.knarcraft.ffmpegconverter.utility;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@ -11,6 +13,7 @@ import java.util.function.Predicate;
|
||||
public final class ListUtil {
|
||||
|
||||
private ListUtil() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -21,7 +24,8 @@ public final class ListUtil {
|
||||
* @param <T> <p>The type of the two lists.</p>
|
||||
* @return <p>A new array containing all elements from the two arrays.</p>
|
||||
*/
|
||||
static <T> T[] concatenate(T[] listA, T[] listB) {
|
||||
@NotNull
|
||||
public static <T> T[] concatenate(@NotNull T[] listA, @NotNull T[] listB) {
|
||||
int listALength = listA.length;
|
||||
int listBLength = listB.length;
|
||||
@SuppressWarnings("unchecked")
|
||||
@ -39,7 +43,7 @@ public final class ListUtil {
|
||||
* @param <T> <p>The type of the list.</p>
|
||||
* @return <p>A new list containing all matching elements.</p>
|
||||
*/
|
||||
static <T> List<T> getMatching(List<T> list, Predicate<T> predicate) {
|
||||
public static <T> List<T> getMatching(List<T> list, Predicate<T> predicate) {
|
||||
List<T> matching = new ArrayList<>(list);
|
||||
matching.removeIf(predicate.negate());
|
||||
return matching;
|
||||
@ -53,7 +57,7 @@ public final class ListUtil {
|
||||
* @param <T> Anything which can be stored in a list
|
||||
* @return True if at least one element fulfills the predicate
|
||||
*/
|
||||
static <T> boolean listContains(T[] list, Predicate<T> predicate) {
|
||||
public static <T> boolean listContains(@NotNull List<T> list, @NotNull Predicate<T> predicate) {
|
||||
for (T item : list) {
|
||||
if (predicate.test(item)) {
|
||||
return true;
|
||||
@ -68,7 +72,8 @@ public final class ListUtil {
|
||||
* @param string <p>A string which may include commas.</p>
|
||||
* @return <p>A string list.</p>
|
||||
*/
|
||||
public static String[] getListFromCommaSeparatedString(String string) {
|
||||
@NotNull
|
||||
public static String[] getListFromCommaSeparatedString(@NotNull String string) {
|
||||
String[] result;
|
||||
if (string.contains(",")) {
|
||||
result = string.split(",");
|
||||
@ -77,4 +82,5 @@ public final class ListUtil {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package net.knarcraft.ffmpegconverter.utility;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
@ -8,10 +10,12 @@ import java.io.OutputStreamWriter;
|
||||
* A class which helps with outputting information
|
||||
*/
|
||||
public final class OutputUtil {
|
||||
|
||||
private static final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));
|
||||
private static boolean debug;
|
||||
|
||||
private OutputUtil() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -27,11 +31,14 @@ public final class OutputUtil {
|
||||
* Prints something and a newline to the commandline efficiently
|
||||
*
|
||||
* @param input <p>The text to print.</p>
|
||||
* @throws IOException <p>If a write is not possible.</p>
|
||||
*/
|
||||
public static void println(String input) throws IOException {
|
||||
if (!input.equals("")) {
|
||||
public static void println(@NotNull String input) {
|
||||
if (!input.isEmpty()) {
|
||||
try {
|
||||
writer.write(input);
|
||||
} catch (IOException e) {
|
||||
System.out.print(input);
|
||||
}
|
||||
}
|
||||
println();
|
||||
}
|
||||
@ -40,22 +47,27 @@ public final class OutputUtil {
|
||||
* Prints a string
|
||||
*
|
||||
* @param input <p>The string to print.</p>
|
||||
* @throws IOException <p>If the writer fails to write.</p>
|
||||
*/
|
||||
public static void print(String input) throws IOException {
|
||||
public static void print(@NotNull String input) {
|
||||
try {
|
||||
writer.write(input);
|
||||
writer.flush();
|
||||
} catch (IOException e) {
|
||||
System.out.print(input);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Prints a newline
|
||||
*
|
||||
* @throws IOException <p>If a write is not possible.</p>
|
||||
*/
|
||||
public static void println() throws IOException {
|
||||
public static void println() {
|
||||
try {
|
||||
writer.newLine();
|
||||
writer.flush();
|
||||
} catch (IOException e) {
|
||||
System.out.println();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -71,11 +83,11 @@ public final class OutputUtil {
|
||||
* Prints a message if debug messages should be shown
|
||||
*
|
||||
* @param message <p>The debug message to show.</p>
|
||||
* @throws IOException <p>If a write is not possible.</p>
|
||||
*/
|
||||
public static void printDebug(String message) throws IOException {
|
||||
public static void printDebug(@NotNull String message) {
|
||||
if (debug) {
|
||||
print(message);
|
||||
println(message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.knarcraft.ffmpegconverter.utility;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.parser.ConverterArgument;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
@ -13,15 +14,18 @@ import java.util.Map;
|
||||
public final class Parser {
|
||||
|
||||
private Parser() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This function parses the input to understandable converter instructions
|
||||
*
|
||||
* @param input <p>The input string to parse.</p>
|
||||
* @param validArguments <p>All arguments which are considered valid.</p>
|
||||
* @return <p>A map with all parsed arguments.</p>
|
||||
*/
|
||||
static Map<String, String> parse(String input, List<ConverterArgument> validArguments) {
|
||||
@NotNull
|
||||
static Map<String, String> parse(@NotNull String input, @NotNull List<ConverterArgument> validArguments) {
|
||||
return parse(tokenize(input), validArguments);
|
||||
}
|
||||
|
||||
@ -32,7 +36,8 @@ public final class Parser {
|
||||
* @param validArguments <p>A list of arguments which are considered valid.</p>
|
||||
* @return <p>A map with all parsed arguments.</p>
|
||||
*/
|
||||
private static Map<String, String> parse(List<String> tokens, List<ConverterArgument> validArguments) {
|
||||
@NotNull
|
||||
private static Map<String, String> parse(@NotNull List<String> tokens, @NotNull List<ConverterArgument> validArguments) {
|
||||
Map<String, String> parsedArguments = new HashMap<>();
|
||||
|
||||
while (!tokens.isEmpty()) {
|
||||
@ -48,7 +53,8 @@ public final class Parser {
|
||||
* @param converterArguments <p>A list of all the valid arguments in existence.</p>
|
||||
* @param parsedArguments <p>The map to store the parsed argument to.</p>
|
||||
*/
|
||||
private static void parseArgument(List<String> tokens, List<ConverterArgument> converterArguments, Map<String, String> parsedArguments) {
|
||||
private static void parseArgument(@NotNull List<String> tokens, @NotNull List<ConverterArgument> converterArguments,
|
||||
@NotNull Map<String, String> parsedArguments) {
|
||||
String currentToken = tokens.remove(0);
|
||||
List<ConverterArgument> foundArguments;
|
||||
|
||||
@ -77,7 +83,8 @@ public final class Parser {
|
||||
* @param foundArgument <p>The found argument to store.</p>
|
||||
* @param parsedArguments <p>The map to store parsed arguments to.</p>
|
||||
*/
|
||||
private static void storeArgumentValue(List<String> tokens, ConverterArgument foundArgument, Map<String, String> parsedArguments) {
|
||||
private static void storeArgumentValue(@NotNull List<String> tokens, @NotNull ConverterArgument foundArgument,
|
||||
@NotNull Map<String, String> parsedArguments) {
|
||||
String argumentValue;
|
||||
if (tokens.isEmpty()) {
|
||||
argumentValue = "";
|
||||
@ -112,7 +119,8 @@ public final class Parser {
|
||||
* @param input <p>A string.</p>
|
||||
* @return <p>A list of tokens.</p>
|
||||
*/
|
||||
public static List<String> tokenize(String input) {
|
||||
@NotNull
|
||||
public static List<String> tokenize(@NotNull String input) {
|
||||
List<String> tokens = new ArrayList<>();
|
||||
boolean startedQuote = false;
|
||||
StringBuilder currentToken = new StringBuilder();
|
||||
@ -155,8 +163,8 @@ public final class Parser {
|
||||
* @param index <p>The index of the read character.</p>
|
||||
* @param tokens <p>The list of processed tokens.</p>
|
||||
*/
|
||||
private static void tokenizeNormalCharacter(StringBuilder currentToken, char character, int inputLength, int index,
|
||||
List<String> tokens) {
|
||||
private static void tokenizeNormalCharacter(@NotNull StringBuilder currentToken, char character, int inputLength,
|
||||
int index, @NotNull List<String> tokens) {
|
||||
currentToken.append(character);
|
||||
if (index == inputLength - 1) {
|
||||
tokens.add(currentToken.toString());
|
||||
@ -171,7 +179,8 @@ public final class Parser {
|
||||
* @param tokens <p>The list of processed tokens.</p>
|
||||
* @return <p>True if the token is finished.</p>
|
||||
*/
|
||||
private static boolean tokenizeSpace(boolean startedQuote, StringBuilder currentToken, List<String> tokens) {
|
||||
private static boolean tokenizeSpace(boolean startedQuote, @NotNull StringBuilder currentToken,
|
||||
@NotNull List<String> tokens) {
|
||||
if (!startedQuote) {
|
||||
//If not inside "", a space marks the end of a parameter
|
||||
if (isNotEmpty(currentToken)) {
|
||||
@ -190,7 +199,8 @@ public final class Parser {
|
||||
* @param builder <p>The string builder to check.</p>
|
||||
* @return <p>True if the string builder is non empty.</p>
|
||||
*/
|
||||
private static boolean isNotEmpty(StringBuilder builder) {
|
||||
return !builder.toString().trim().equals("");
|
||||
private static boolean isNotEmpty(@NotNull StringBuilder builder) {
|
||||
return !builder.toString().trim().isEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,32 +1,42 @@
|
||||
package net.knarcraft.ffmpegconverter.utility;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A class which helps with operations on strings
|
||||
*/
|
||||
final class StringUtil {
|
||||
public final class StringUtil {
|
||||
|
||||
private StringUtil() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all substrings between two substrings in a string
|
||||
*
|
||||
* @param string <p>The string containing the substrings.</p>
|
||||
* @param input <p>The string containing the substrings.</p>
|
||||
* @param start <p>The substring before the wanted substring.</p>
|
||||
* @param end <p>The substring after the wanted substring.</p>
|
||||
* @return <p>A list of all occurrences of the substring.</p>
|
||||
*/
|
||||
static String[] stringBetween(String string, String start, String end) {
|
||||
int startPosition = string.indexOf(start) + start.length();
|
||||
public static @NotNull List<String> stringBetween(@NotNull String input, @NotNull String start,
|
||||
@NotNull String end) {
|
||||
List<String> output = new ArrayList<>();
|
||||
String inputString = input;
|
||||
while (true) {
|
||||
int startPosition = inputString.indexOf(start) + start.length();
|
||||
//Return if the string is not found
|
||||
if (!string.contains(start) || string.indexOf(end, startPosition) < startPosition) {
|
||||
return new String[]{};
|
||||
if (!inputString.contains(start) || inputString.indexOf(end, startPosition) < startPosition) {
|
||||
return output;
|
||||
}
|
||||
int endPosition = string.indexOf(end, startPosition);
|
||||
int endPosition = inputString.indexOf(end, startPosition);
|
||||
//Get the string between the start and end string
|
||||
String outString = string.substring(startPosition, endPosition).trim();
|
||||
String nextString = string.substring(endPosition + end.length());
|
||||
//Add other occurrences recursively
|
||||
return ListUtil.concatenate(new String[]{outString}, stringBetween(nextString, start, end));
|
||||
output.add(inputString.substring(startPosition, endPosition).trim());
|
||||
inputString = inputString.substring(endPosition + end.length());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,30 @@
|
||||
package net.knarcraft.ffmpegconverter.utility;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A helper class for subtitle-related tasks
|
||||
*/
|
||||
public final class SubtitleHelper {
|
||||
|
||||
private SubtitleHelper() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given subtitle is a songs & signs subtitle
|
||||
*
|
||||
* @param subtitleTitle <p>The subtitle to check</p>
|
||||
* @return <p>True if the subtitle is a songs and signs, not a full subtitle</p>
|
||||
*/
|
||||
public static boolean isSongsSignsSubtitle(@NotNull String subtitleTitle) {
|
||||
Pattern pattern = Pattern.compile("(^| |\\(|\\[\\{|/|\\[)si(ng|gn)s?($|[ &/+-@])+(titles)?[ &/+-@]?(songs?)?|" +
|
||||
"(^| |\\(|\\[\\{|/)songs?($|[ &/+-@])+(si(gn|ng)s?)?|.*forced.*|.*s&s.*");
|
||||
Matcher matcher = pattern.matcher(subtitleTitle.toLowerCase().trim());
|
||||
return matcher.find();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
package net.knarcraft.ffmpegconverter.utility;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A helper class for parsing values without causing errors
|
||||
*/
|
||||
public final class ValueParsingHelper {
|
||||
|
||||
private ValueParsingHelper() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an integer from a string
|
||||
*
|
||||
* @param input <p>The input given</p>
|
||||
* @param defaultValue <p>The default value to return if no integer could be parsed</p>
|
||||
* @return <p>The parsed integer, or the given default value</p>
|
||||
*/
|
||||
public static int parseInt(@Nullable String input, int defaultValue) {
|
||||
if (input == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
try {
|
||||
return Integer.parseInt(input);
|
||||
} catch (NumberFormatException exception) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a string
|
||||
*
|
||||
* @param input <p>The input string</p>
|
||||
* @param defaultValue <p>The default value to use if the string is null</p>
|
||||
* @return <p>The input string, or the default value</p>
|
||||
*/
|
||||
@NotNull
|
||||
public static String parseString(@Nullable String input, @NotNull String defaultValue) {
|
||||
return Objects.requireNonNullElse(input, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a boolean
|
||||
*
|
||||
* @param input <p>The input string</p>
|
||||
* @param defaultValue <p>The default value to use if the string is null</p>
|
||||
* @return <p>The parsed boolean, or the default value</p>
|
||||
*/
|
||||
public static boolean parseBoolean(@Nullable String input, boolean defaultValue) {
|
||||
if (input == null || input.isEmpty()) {
|
||||
return defaultValue;
|
||||
} else {
|
||||
return Boolean.parseBoolean(input);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
39
|
||||
3gp
|
||||
aa
|
||||
aac
|
||||
@ -38,3 +37,5 @@ wma
|
||||
wv
|
||||
webm
|
||||
8svx
|
||||
mka
|
||||
ac3
|
24
src/main/resources/conf/config.properties
Normal file
24
src/main/resources/conf/config.properties
Normal file
@ -0,0 +1,24 @@
|
||||
# Enabling debug mode will only output part of videos, so different settings can be tested more quickly.
|
||||
# Debug mode also prints more information about probe results, and potentially useful output.
|
||||
debug=false
|
||||
# Enabling hardware acceleration will try to use hardware acceleration for converters where it's available. Note that
|
||||
# software encoders generally produce a lower file-size relative to the output quality.
|
||||
hardware-acceleration-encode=false
|
||||
# Hardware decoding can often speed up the conversion, but might be troublesome at times
|
||||
hardware-acceleration-decode=true
|
||||
# The available hardware encoders
|
||||
encoders-hardware-accelerated=qsv,cuda,vaapi,dxva2,d3d11va,opencl,vulkan,d3d12va
|
||||
# As FLAC can increase file size significantly, this option enabled automatic re-encode of flac tracks
|
||||
encode-flac-always=false
|
||||
# The preference for audio languages when converting anime (0 = undefined, * = any)
|
||||
audio-languages-anime=jpn,eng,*
|
||||
# The preference for subtitle languages when converting anime (0 = undefined, * = any)
|
||||
subtitle-languages-anime=eng,*
|
||||
# The preference for minimal subtitles, AKA Signs & Songs (REQUIRE/PREFER/NO_PREFERENCE/AVOID/REJECT)
|
||||
minimal-subtitle-preference=AVOID
|
||||
# The preference for whether video streams should be de-interlaced. It is recommended to only enable this when you notice that a video file is interlaced.
|
||||
de-interlace-video=false
|
||||
# Whether to copy attached cover images. FFMpeg sometimes throws errors when including attached images.
|
||||
copy-attached-images=false
|
||||
# Whether to overwrite original files after conversion. Note that if enabled, the original files are lost, which is troublesome if the conversion arguments are incorrect.
|
||||
overwrite-files=false
|
@ -1,5 +1,5 @@
|
||||
4
|
||||
idx
|
||||
sub
|
||||
srt
|
||||
ass
|
||||
vtt
|
@ -1,4 +1,3 @@
|
||||
31
|
||||
avi
|
||||
mpg
|
||||
mpeg
|
||||
@ -30,3 +29,4 @@ svi
|
||||
3g2
|
||||
roq
|
||||
nsv
|
||||
mpeg4
|
@ -0,0 +1,47 @@
|
||||
package net.knarcraft.ffmpegconverter.utility;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class SubtitleHelperTest {
|
||||
|
||||
@Test
|
||||
public void isSignsSongsSubtitlePositiveTest() {
|
||||
assertTrue(SubtitleHelper.isSongsSignsSubtitle("Signs"));
|
||||
assertTrue(SubtitleHelper.isSongsSignsSubtitle("Songs"));
|
||||
assertTrue(SubtitleHelper.isSongsSignsSubtitle("Sign"));
|
||||
assertTrue(SubtitleHelper.isSongsSignsSubtitle("Song"));
|
||||
assertTrue(SubtitleHelper.isSongsSignsSubtitle("Signs & Songs"));
|
||||
assertTrue(SubtitleHelper.isSongsSignsSubtitle("Songs & Signs"));
|
||||
assertTrue(SubtitleHelper.isSongsSignsSubtitle("Sings & Songs"));
|
||||
assertTrue(SubtitleHelper.isSongsSignsSubtitle("Songs & Sings"));
|
||||
assertTrue(SubtitleHelper.isSongsSignsSubtitle("Signs/Titles/Songs"));
|
||||
assertTrue(SubtitleHelper.isSongsSignsSubtitle("Signs@"));
|
||||
assertTrue(SubtitleHelper.isSongsSignsSubtitle("Songs"));
|
||||
assertTrue(SubtitleHelper.isSongsSignsSubtitle("Sings & Songs@"));
|
||||
assertTrue(SubtitleHelper.isSongsSignsSubtitle("Songs & Sings -"));
|
||||
assertTrue(SubtitleHelper.isSongsSignsSubtitle("Signs/Titles/Songs bla bla (bla)"));
|
||||
assertTrue(SubtitleHelper.isSongsSignsSubtitle("English (Signs/Titles/Songs)"));
|
||||
assertTrue(SubtitleHelper.isSongsSignsSubtitle("Forced"));
|
||||
assertTrue(SubtitleHelper.isSongsSignsSubtitle("Signs / Songs"));
|
||||
assertTrue(SubtitleHelper.isSongsSignsSubtitle("Songs / Signs"));
|
||||
assertTrue(SubtitleHelper.isSongsSignsSubtitle("Signs/Lyrics"));
|
||||
assertTrue(SubtitleHelper.isSongsSignsSubtitle("English [Signs/Lyrics]"));
|
||||
assertTrue(SubtitleHelper.isSongsSignsSubtitle("Forced Subtitles"));
|
||||
assertTrue(SubtitleHelper.isSongsSignsSubtitle("Some Text Signs & Songs"));
|
||||
assertTrue(SubtitleHelper.isSongsSignsSubtitle("Some Text Songs & Signs"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isSignsSongsSubtitleNegativeTest() {
|
||||
assertFalse(SubtitleHelper.isSongsSignsSubtitle("Potato"));
|
||||
assertFalse(SubtitleHelper.isSongsSignsSubtitle("assign"));
|
||||
assertFalse(SubtitleHelper.isSongsSignsSubtitle("signed"));
|
||||
assertFalse(SubtitleHelper.isSongsSignsSubtitle("English"));
|
||||
assertFalse(SubtitleHelper.isSongsSignsSubtitle("Full subtitle"));
|
||||
assertFalse(SubtitleHelper.isSongsSignsSubtitle("Dialogue"));
|
||||
}
|
||||
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
package net.knarcraft.ffmpegconverter.converter;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class AnimeConverterTest {
|
||||
@Before
|
||||
public void setUp() {
|
||||
AnimeConverter converter = new AnimeConverter("ffprobe", "ffmpeg",
|
||||
new String[]{"jpn", "eng", "nor", "swe"}, new String[]{"nor", "eng", "swe", "fin"}, false,
|
||||
false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void weirdTest() {
|
||||
assertEquals(0, 0);
|
||||
}
|
||||
}
|
@ -13,7 +13,7 @@ import static org.junit.Assert.assertFalse;
|
||||
|
||||
public class ListUtilTest {
|
||||
private static List<Integer> matchesList;
|
||||
private static Integer[] containsList;
|
||||
private static List<Integer> containsList;
|
||||
|
||||
@BeforeClass
|
||||
public static void setUp() {
|
||||
@ -28,7 +28,7 @@ public class ListUtilTest {
|
||||
matchesList.add(19);
|
||||
matchesList.add(21);
|
||||
matchesList.add(23);
|
||||
containsList = new Integer[]{1, 3, 5, 7, 234, 23, 45};
|
||||
containsList = List.of(1, 3, 5, 7, 234, 23, 45);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -1,7 +1,7 @@
|
||||
package net.knarcraft.ffmpegconverter.utility;
|
||||
|
||||
import net.knarcraft.ffmpegconverter.parser.ConverterArgument;
|
||||
import net.knarcraft.ffmpegconverter.parser.ConverterArgumentValue;
|
||||
import net.knarcraft.ffmpegconverter.parser.ConverterArgumentValueType;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
@ -18,9 +18,9 @@ public class ParserTest {
|
||||
@Before
|
||||
public void setUp() {
|
||||
validArguments = new ArrayList<>();
|
||||
validArguments.add(new ConverterArgument("anargument", 'a', true, ConverterArgumentValue.STRING));
|
||||
validArguments.add(new ConverterArgument("turnoff", 't', false, ConverterArgumentValue.BOOLEAN));
|
||||
validArguments.add(new ConverterArgument("turnon", 'o', false, ConverterArgumentValue.BOOLEAN));
|
||||
validArguments.add(new ConverterArgument("anargument", 'a', true, ConverterArgumentValueType.STRING));
|
||||
validArguments.add(new ConverterArgument("turnoff", 't', false, ConverterArgumentValueType.BOOLEAN));
|
||||
validArguments.add(new ConverterArgument("turnon", 'o', false, ConverterArgumentValueType.BOOLEAN));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -2,27 +2,33 @@ package net.knarcraft.ffmpegconverter.utility;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class StringUtilTest {
|
||||
|
||||
@Test
|
||||
public void stringBetweenNoMatches() {
|
||||
String[] result = StringUtil.stringBetween("a test string", "[", "]");
|
||||
assertArrayEquals(new String[]{}, result);
|
||||
List<String> result = StringUtil.stringBetween("a test string", "[", "]");
|
||||
List<String> empty = new ArrayList<>();
|
||||
assertEquals(empty, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stringBetweenOneMatch() {
|
||||
String[] result = StringUtil.stringBetween("a [test] string", "[", "]");
|
||||
assertArrayEquals(new String[]{"test"}, result);
|
||||
List<String> result = StringUtil.stringBetween("a [test] string", "[", "]");
|
||||
List<String> expected = List.of("test");
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stringBetweenSeveralMatches() {
|
||||
String[] result = StringUtil.stringBetween("a long string containing a lot of potential matches for " +
|
||||
List<String> result = StringUtil.stringBetween("a long string containing a lot of potential matches for " +
|
||||
"the string between method defined in the StringUtil class", " ", "a");
|
||||
assertArrayEquals(new String[]{"long string cont", "", "lot of potenti", "m",
|
||||
"for the string between method defined in the StringUtil cl"}, result);
|
||||
List<String> expected = List.of("long string cont", "", "lot of potenti", "m",
|
||||
"for the string between method defined in the StringUtil cl");
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user