Compare commits

..

41 Commits

Author SHA1 Message Date
4fdbfb28e3 Adds an audio extractor
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
2024-10-12 01:37:40 +02:00
87f5743a24 Adds option for overwriting original files
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
2024-07-18 14:07:51 +02:00
972691db76 Removes inclusion of external audio files, as ffmpeg produces audio streams with no sound
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
2024-07-10 19:53:03 +02:00
2145bfb8ea Improves handling of cover images
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
2024-07-09 19:37:05 +02:00
c0249c3b3a Adjusts the Signs/Songs RegEx
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
2024-07-06 01:55:02 +02:00
da67b195de Adds a converter for converting video audio to vorbis 2024-07-05 11:11:20 +02:00
5238697c70 Automatically fixes jap misspelling for Japanese streams
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
2024-06-10 14:36:19 +02:00
32be7d0aec Slightly alters the signs & songs RegEx
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
2024-06-09 03:44:09 +02:00
1ceb378757 Various smaller improvements
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
Improves Signs & Songs subtitle RegEx
Adds tests for the Signs & Songs subtitle RegEx
Automatically changes "enm" language to English
Makes the downscale converter select HEVC if the input is HEVC
2024-06-07 15:46:51 +02:00
1dc489a6f8 Adds a de-interlace module and stuff
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
Adds a module to de-interlace when converting a video. It's enabled by a new configuration option.
Fixes some bugs in the letterbox cropper
Adds mpeg4 to video formats
2024-05-29 17:06:32 +02:00
c0c8c9c054 Adds a Letterbox cropper, and fixes cover images for the anime converter
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
Adds a new letterbox cropper, which will use 10 samples at 30 points in a video in order to find the correct crop to remove letter-boxing.
Makes sure to always copy codec of cover images, as ffmpeg treats them as video streams.
2024-05-25 00:15:41 +02:00
c3c89fcb75 Adds a new converter for purely re-ordering and filtering streams
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
2024-05-20 19:33:51 +02:00
380a1b800a Improves configuration handling and stuff
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
2024-05-19 10:16:51 +02:00
ac25ca1986 Changes some things
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
Removes any forced subtitles when changing the default stream
Makes the title filter accept a list of filters, and allows using it for sorting rather than filtering
2024-05-06 01:32:19 +02:00
dae93b9f81 Adds filtering for audio description tracks
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
2024-05-05 03:50:07 +02:00
d46f12e690 Copies audio and other streams for the MKV to H265 reduced converter 2024-04-23 14:00:15 +02:00
92b46bdc9e Adds external audio parsing and other fixes
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
Trims subtitle tiles before checking if it's full
Generalizes some stream parsing
Fixes an exception when a stream tag is set, but has no value
Looks for both subtitle and audio streams adjacent to the main file
2024-04-21 19:54:45 +02:00
ded88eb5b5 Makes the anime converter map unknown streams
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
2024-04-20 00:43:52 +02:00
3c9fa55585 Fixes various issues
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
Makes it possible to turn off all types of hardware acceleration for all converters
Only encodes anime to hevc if not already hevc
Adds an option to force hevc to hevc encoding, for the anime converter
Adds an option to force encoding of audio, for the anime converter
Fixes a bug causing the codec name to not be parsed
Fixes an exception when trying to sort an empty list
Fixes order of sorting in the anime converter
Adds another signs songs filter
2024-04-19 13:05:12 +02:00
d487df0e78 Attempts to fix compilation
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
2024-04-17 15:57:28 +02:00
4ebd29b358 Makes it possible to enable debug mode
Some checks failed
KnarCraft/FFmpegConvert/pipeline/head There was a failure building this commit
Debug mode is now enabled if the property `debug = true` is set in `conf/config.properties`
Changes code for reading internal configurations
Changes many primitive lists to List<>
Adds some missing annotations
Renames the main class
2024-04-17 15:35:09 +02:00
f0e75eb440 Adds a new type of anime converter and stuff
Some checks failed
KnarCraft/FFmpegConvert/pipeline/head There was a failure building this commit
Adds a new type of anime converter that retains all streams, but converts the video to hevc, and re-orders the streams
Adds support for several types of encoding hardware acceleration that are automatically disabled if not available on the system.
Adds automatic hardware decoding acceleration.
Automatically removes empty files if ffmpeg fails.
2024-04-17 04:39:42 +02:00
461c7552b3 Adds converter modules and sorters
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
Converters are now the result of combining multiple modules, and running them in the selected sequence. I hope to be able to completely remove converters by replacing them with templates that simply run some modules in a sequence, before running FFmpeg. Then I want to be able to parse a command into a template, so users have full control of what a template does.

Sorters each sort a list of streams after a single criteria. They can be chained in order to create complex chained sorters.
2024-04-12 15:31:34 +02:00
376d5655f2 Improves FFMpeg command generation
Adds an object for storing FFmpeg variables. This allows any argument type to be added at any time, removing the limitation of having to add to the command in a specific order.
Makes stream objects store the index of the input file they belong to.
Saves the result of probes to a record that's easy to pass around.
Always passes all probed files to the input files of ffmpeg.
Makes it easier to enable h26x encoding/decoding when necessary.
Removes the hard-coded behavior for external subtitles, and allows any stream to be an external stream.
2024-04-08 20:00:09 +02:00
be88845731 Makes getting any stream info much easier
Adds some methods for parsing Strings as other objects without resulting in exceptions.
Adds a class for representing all possible stream info tags.
Makes streams parse data themselves, after receiving all tags set for the stream.
Changes Java version to Java 16
2024-04-08 00:47:48 +02:00
2c75d91cce Adds a converter to embed subtitles
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
2024-03-27 12:36:11 +01:00
2962114601 Simplifies pom.xml somewhat 2024-03-15 03:44:55 +01:00
692d9e79d2 Fixes some warnings 2024-03-15 02:09:46 +01:00
6c614b2f17 Adds a video downscaler
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
2024-03-15 01:20:26 +01:00
346a5e0606 Adds another signs and songs match
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
2024-02-25 22:20:58 +01:00
2346e651ef Adds new converters
Adds a MKV to HEVC converter which aims to reduce file-sizes
Adds a MKV to MP4 transcoder which allows selection of each type of stream
2023-11-14 17:10:45 +01:00
f81a21b9e9 Makes extension check case insensitive 2023-08-31 03:03:59 +02:00
56f5e31934 Fixes subtitle mapping for video with only external subtitles
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
2023-08-12 22:14:46 +02:00
5d94cabca0 Adds a MKV to h264 MKV converter
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
2023-07-27 17:39:18 +02:00
a9ea1f796a Makes sure to catch a signs misspelling when looking for signs & songs
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
2023-07-15 16:42:31 +02:00
32ec50ba7d Fixes incorrect video stream index
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
This fixes the absolute video stream index being used instead of the relative video stream index when converting a video with image (PGS) subtitles.
2022-12-23 02:09:29 +01:00
3c298f623e Adds ability to filter subtitle streams based on the title
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
2022-10-11 23:13:45 +02:00
1323513e46 Adds ability to force-select specific audio and subtitle streams for the anime converter
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
2022-10-09 23:53:39 +02:00
388563574f Fixes case of a songs/signs check
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
2022-09-30 21:48:20 +02:00
750498810c Adds a new Songs and Signs check
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
2022-09-30 03:55:32 +02:00
c9bd648437 Fixes only getting a partial regex match when deciding whether a subtitle is partial/forced or not
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
2021-01-26 18:12:34 +01:00
99 changed files with 5677 additions and 1239 deletions

View File

@ -1,3 +0,0 @@
Manifest-Version: 1.0
Main-Class: net.knarcraft.ffmpegconverter.Main

View File

@ -1,3 +0,0 @@
Manifest-Version: 1.0
X-COMMENT: Main-Class will be added automatically by build

82
pom.xml
View File

@ -6,7 +6,6 @@
<groupId>net.knarcraft.ffmpegconvert</groupId> <groupId>net.knarcraft.ffmpegconvert</groupId>
<artifactId>ffmpegconvert</artifactId> <artifactId>ffmpegconvert</artifactId>
<version>0.1-alpha</version> <version>0.1-alpha</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>FFMpeg Convert</name> <name>FFMpeg Convert</name>
@ -33,8 +32,7 @@
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source> <java.version>16</java.version>
<maven.compiler.target>1.8</maven.compiler.target>
</properties> </properties>
<repositories> <repositories>
@ -64,44 +62,76 @@
<build> <build>
<plugins> <plugins>
<plugin> <plugin>
<!-- Build an executable JAR -->
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<version>3.2.0</version> <version>3.8.1</version>
<configuration> <configuration>
<archive> <source>${java.version}</source>
<manifest> <target>${java.version}</target>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<mainClass>net.knarcraft.ffmpegconverter.Main</mainClass>
</manifest>
</archive>
</configuration> </configuration>
</plugin> </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>
<mainClass>net.knarcraft.ffmpegconverter.FFMpegConvert</mainClass>
</manifest>
</archive>
</configuration>
</execution>
</executions>
</plugin>
</plugins> </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> <resources>
<resource> <resource>
<directory>${project.basedir}/src/main/resources</directory> <directory>src/main/resources</directory>
<filtering>true</filtering>
</resource> </resource>
</resources> </resources>
<testResources>
<testResource>
<directory>${project.basedir}/src/test/resources</directory>
</testResource>
</testResources>
</build> </build>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>junit</groupId> <groupId>junit</groupId>
<artifactId>junit</artifactId> <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> <scope>test</scope>
</dependency> </dependency>
</dependencies> </dependencies>

View 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;
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}
}
}

View File

@ -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]);
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}
}

View File

@ -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) {
}

View File

@ -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;
}
}

View File

@ -1,12 +1,18 @@
package net.knarcraft.ffmpegconverter.converter; 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.StreamObject;
import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
import net.knarcraft.ffmpegconverter.streams.VideoStream;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper; import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
import net.knarcraft.ffmpegconverter.utility.FileHelper;
import net.knarcraft.ffmpegconverter.utility.FileUtil; import net.knarcraft.ffmpegconverter.utility.FileUtil;
import net.knarcraft.ffmpegconverter.utility.OutputUtil; import net.knarcraft.ffmpegconverter.utility.OutputUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -17,171 +23,149 @@ import java.util.List;
* Implements all methods which can be useful for any implementation of a converter. * Implements all methods which can be useful for any implementation of a converter.
*/ */
public abstract class AbstractConverter implements Converter { public abstract class AbstractConverter implements Converter {
final boolean debug = false;
private final String outputExtension; final boolean debug = FFMpegConvert.getConfiguration().isDebugEnabled();
String ffprobePath; private final String newExtension;
String ffmpegPath; protected String ffprobePath;
String[] audioFormats; protected String ffmpegPath;
String[] videoFormats; protected List<String> audioFormats;
protected List<String> videoFormats;
protected List<String> subtitleFormats;
protected AvailableHardwareEncoderHandler encoderHandler = null;
/** /**
* Initializes variables used by the abstract converter * Initializes variables used by the abstract converter
*/ */
AbstractConverter(String outputExtension) { AbstractConverter(@Nullable String newExtension) {
this.outputExtension = outputExtension; this.newExtension = newExtension;
OutputUtil.setDebug(this.debug);
try { try {
audioFormats = FileUtil.readFileLines("audio_formats.txt"); this.audioFormats = FileHelper.readLines(FileHelper.getBufferedReaderForInternalFile("/audio_formats.txt"));
videoFormats = FileUtil.readFileLines("video_formats.txt"); this.videoFormats = FileHelper.readLines(FileHelper.getBufferedReaderForInternalFile("/video_formats.txt"));
this.subtitleFormats = FileHelper.readLines(FileHelper.getBufferedReaderForInternalFile("/subtitle_formats.txt"));
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); OutputUtil.println("Unable to read audio and/or video formats from internal files.");
System.exit(1); System.exit(1);
} }
} }
/** @Override
* Filters parsed streams into one of the stream types public void convert(@NotNull File file) throws IOException {
* StreamProbeResult probeResult = FFMpegHelper.probeFile(this.ffprobePath, file, this.subtitleFormats);
* @param streams <p>A list of stream objects.</p> if (probeResult.parsedStreams().isEmpty()) {
* @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()) {
throw new IllegalArgumentException("The file has no valid streams. Please make sure the file exists and" + throw new IllegalArgumentException("The file has no valid streams. Please make sure the file exists and" +
" is not corrupt."); " is not corrupt.");
} }
String newPath = FileUtil.getNonCollidingPath(folder, file, outputExtension);
String outExtension = this.newExtension != null ? this.newExtension : FileUtil.getExtension(file.getName());
String newPath = FileUtil.getNonCollidingPath(file.getParentFile(), file, outExtension);
OutputUtil.println(); OutputUtil.println();
OutputUtil.println("Preparing to start process..."); OutputUtil.println("Preparing to start process...");
OutputUtil.println("Converting " + file); 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 audioStreams <p>A list of all streams.</p> * @param ffMpegCommand <p>The failed ffmpeg command</p>
* @return <p>The first audio stream found or null if no audio streams were found.</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 getFirstAudioStream(List<AudioStream> audioStreams) { private void handleError(@NotNull FFMpegCommand ffMpegCommand, @NotNull File file,
AudioStream audioStream = null; @NotNull String newPath) throws IOException {
if (audioStreams.size() > 0) { File outputFile = new File(newPath);
audioStream = audioStreams.get(0); 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 subtitleStreams <p>A list of all subtitle streams.</p> * @return <p>Available hardware encoding methods</p>
* @return <p>The first subtitle stream found or null if no subtitle streams were found.</p>
*/ */
SubtitleStream getFirstSubtitleStream(List<SubtitleStream> subtitleStreams) { @NotNull
SubtitleStream subtitleStream = null; protected List<String> getAvailableHardwareEncodingMethods() {
if (subtitleStreams.size() > 0) { if (encoderHandler == null) {
subtitleStream = subtitleStreams.get(0); encoderHandler = AvailableHardwareEncoderHandler.load();
if (encoderHandler.availableHardwareEncodings().isEmpty()) {
List<String> hardwareEncoding;
try {
hardwareEncoding = new ArrayList<>(FFMpegHelper.getHWAcceleration(ffmpegPath));
} catch (IOException e) {
throw new RuntimeException(e);
}
hardwareEncoding.remove(0);
encoderHandler = new AvailableHardwareEncoderHandler(hardwareEncoding);
encoderHandler.save();
}
} }
return subtitleStream;
return encoderHandler.availableHardwareEncodings();
} }
/** /**
* Gets the first video stream from a list of streams * Sets the output indexes for the given streams
* *
* @param videoStreams <p>A list of all streams.</p> * <p>This will basically mark the given streams' order as the order they will appear in the output file. This is
* @return <p>The first video stream found or null if no video streams were found.</p> * 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<VideoStream> videoStreams) { protected <K extends StreamObject> void setOutputIndexes(@NotNull List<K> streams) {
VideoStream videoStream = null; for (int i = 0; i < streams.size(); i++) {
if (videoStreams.size() > 0) { streams.get(i).setOutputIndex(i);
videoStream = videoStreams.get(0);
} }
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);
}
} }

View File

@ -1,122 +0,0 @@
package net.knarcraft.ffmpegconverter.converter;
import net.knarcraft.ffmpegconverter.streams.AudioStream;
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.ListUtil;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class AdvancedConverter extends AbstractConverter {
private boolean burnFirstSubtitle = true;
private boolean burnFirstAudio = true;
private String videoCodec = "h264";
private String pixelFormat = "yuv420p";
private int audioSamplingFrequency = 48000;
private boolean moveHeaders = true;
private boolean convertToStereo = true;
private boolean preventPartialSubtitles = true;
private String[] audioLanguages = new String[]{"jpn", "eng", "0"};
private String[] subtitleLanguages = new String[]{"eng", "0"};
private String outputExtension = "mp4";
private boolean autoStreamSelection = false;
/**
* Initializes variables used by the abstract converter
*/
AdvancedConverter(Map<String, String> inputArguments) {
super(inputArguments.get("outputextension"));
}
@Override
public String[] generateConversionCommand(String executable, File file, List<StreamObject> streams, String outFile) {
List<String> command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, file.getName());
if (this.debug) {
FFMpegHelper.addDebugArguments(command, 50, 120);
}
if (!this.videoCodec.isEmpty()) {
command.add("-vcodec");
command.add(this.videoCodec);
}
if (!this.pixelFormat.isEmpty()) {
command.add("-pix_fmt");
command.add(this.pixelFormat);
}
if (this.audioSamplingFrequency > 0) {
command.add("-ar");
command.add("" + this.audioSamplingFrequency);
}
if (this.moveHeaders) {
command.add("-movflags");
command.add("+faststart");
}
//If the user wants to convert to an audio file, just select audio streams
if (ListUtil.listContains(audioFormats, (item) -> item.equals(this.outputExtension)) || autoStreamSelection) {
command.add(outFile);
return command.toArray(new String[0]);
}
if (!burnFirstAudio && !burnFirstSubtitle) {
//Copy all streams
command.add("-map");
command.add("0");
command.add("-c:a");
command.add("copy");
command.add("-c:s");
command.add("copy");
} else {
//Get the first audio stream in accordance with chosen languages
List<AudioStream> audioStreams = filterAudioStreams(filterStreamsByType(streams, AudioStream.class), audioLanguages);
AudioStream audioStream = getFirstAudioStream(new ArrayList<>(audioStreams));
//Get the first subtitle stream in accordance with chosen languages and signs and songs prevention
List<SubtitleStream> subtitleStreams = filterSubtitleStreams(filterStreamsByType(streams,
SubtitleStream.class), this.subtitleLanguages, this.preventPartialSubtitles);
SubtitleStream subtitleStream = getFirstSubtitleStream(new ArrayList<>(subtitleStreams));
//Get the first video stream
VideoStream videoStream = getFirstVideoStream(filterStreamsByType(streams, VideoStream.class));
//Add streams to output file
if (burnFirstAudio) {
FFMpegHelper.addAudioStream(command, audioStream, this.convertToStereo);
} else {
command.add("-map");
command.add("0:a");
command.add("-c:a");
command.add("copy");
}
if (burnFirstSubtitle) {
FFMpegHelper.addSubtitleAndVideoStream(command, subtitleStream, videoStream, file);
} else {
command.add("-map");
command.add("0:s");
command.add("-map");
command.add("0:v");
command.add("-c:s");
command.add("copy");
}
}
command.add(outFile);
return command.toArray(new String[0]);
}
@Override
public String[] getValidFormats() {
return ListUtil.concatenate(videoFormats, audioFormats);
}
}

View File

@ -1,74 +1,185 @@
package net.knarcraft.ffmpegconverter.converter; 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.AudioStream;
import net.knarcraft.ffmpegconverter.streams.StreamObject; import net.knarcraft.ffmpegconverter.streams.OtherStream;
import net.knarcraft.ffmpegconverter.streams.SubtitleStream; import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
import net.knarcraft.ffmpegconverter.streams.VideoStream; import net.knarcraft.ffmpegconverter.streams.VideoStream;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper; 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.ArrayList;
import java.util.Arrays;
import java.util.List; 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 { public class AnimeConverter extends AbstractConverter {
private final String[] audioLanguages;
private final String[] subtitleLanguages; private final List<String> audioLanguages;
private final boolean toStereo; private final List<String> subtitleLanguages;
private final boolean preventSignsAndSongs; 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 * Instantiates a new anime converter
* *
* @param ffprobePath <p>Path/command to ffprobe.</p> * @param ffprobePath <p>Path/command to ffprobe.</p>
* @param ffmpegPath <p>Path/command to ffmpeg.</p> * @param ffmpegPath <p>Path/command to ffmpeg.</p>
* @param audioLanguages <p>List of wanted audio languages in descending order.</p> * @param forcedAudioIndex <p>A specific audio stream to force as default. 0-indexed from the first audio stream found</p>
* @param subtitleLanguages <p>List of wanted subtitle languages in descending order.</p> * @param forcedSubtitleIndex <p>A specific subtitle stream to force as default. 0-indexed for the first subtitle stream found</p>
* @param toStereo <p>Convert video with several audio channels to stereo.</p> * @param forceVideoEncoding <p>Whether to enforce encoding on the video, even if already hevc</p>
* @param preventSignsAndSongs <p>Prevent subtitles only converting signs and songs (not speech).</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, public AnimeConverter(@NotNull String ffprobePath, @NotNull String ffmpegPath, int forcedAudioIndex,
boolean toStereo, boolean preventSignsAndSongs) { int forcedSubtitleIndex, @NotNull String subtitleNameFilter, boolean forceVideoEncoding,
super("mp4"); boolean forceAudioEncoding) {
super("mkv");
Configuration configuration = FFMpegConvert.getConfiguration();
this.ffprobePath = ffprobePath; this.ffprobePath = ffprobePath;
this.ffmpegPath = ffmpegPath; this.ffmpegPath = ffmpegPath;
this.audioLanguages = audioLanguages; this.audioLanguages = configuration.getAnimeAudioLanguages();
this.subtitleLanguages = subtitleLanguages; this.subtitleLanguages = configuration.getAnimeSubtitleLanguages();
this.toStereo = toStereo; this.subtitlePreference = configuration.getMinimalSubtitlePreference();
this.preventSignsAndSongs = preventSignsAndSongs; this.forcedAudioIndex = forcedAudioIndex;
this.forcedSubtitleIndex = forcedSubtitleIndex;
this.subtitleNameFilter = subtitleNameFilter;
this.forceVideoEncoding = forceVideoEncoding;
this.forceAudioEncoding = forceAudioEncoding;
} }
@Override @Override
public String[] generateConversionCommand(String executable, File file, List<StreamObject> streams, String outFile) { @Nullable
List<String> command = FFMpegHelper.getFFMpegWebVideoCommand(executable, file.getName()); 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) { 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 //Get the first audio stream in accordance with chosen languages
List<AudioStream> audioStreams = filterAudioStreams(filterStreamsByType(streams, AudioStream.class), audioLanguages); StreamSorter<AudioStream> audioSorter = new AudioLanguageSorter(this.audioLanguages)
AudioStream audioStream = getFirstAudioStream(new ArrayList<>(audioStreams)); .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 //Get the first subtitle stream in accordance with chosen languages and signs and songs prevention
List<SubtitleStream> subtitleStreams = filterSubtitleStreams(filterStreamsByType(streams, StreamSorter<SubtitleStream> subtitleSorter = new SubtitleTitleSorter(
SubtitleStream.class), subtitleLanguages, preventSignsAndSongs); List.of(this.subtitleNameFilter.split(",")))
SubtitleStream subtitleStream = getFirstSubtitleStream(new ArrayList<>(subtitleStreams)); .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 if (configuration.useHardwareDecoding()) {
VideoStream videoStream = getFirstVideoStream(filterStreamsByType(streams, VideoStream.class)); modules.add(new HardwareDecodeModule());
}
//Add streams to output file boolean encodeFlac = this.forceAudioEncoding || FFMpegConvert.getConfiguration().alwaysEncodeFlac();
FFMpegHelper.addAudioStream(command, audioStream, toStereo); for (AudioStream audioStream : sortedAudio) {
FFMpegHelper.addSubtitleAndVideoStream(command, subtitleStream, videoStream, file); if (!encodeFlac || !audioStream.getCodecName().equalsIgnoreCase("flac")) {
modules.add(new CopyAudioModule(audioStream));
}
}
modules.add(new CopySubtitlesModule());
command.add(outFile); boolean encodingNecessary = false;
return command.toArray(new String[0]); 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 @Override
public String[] getValidFormats() { @NotNull
return videoFormats; public List<String> getValidFormats() {
return this.videoFormats;
} }
} }

View File

@ -1,10 +1,17 @@
package net.knarcraft.ffmpegconverter.converter; package net.knarcraft.ffmpegconverter.converter;
import net.knarcraft.ffmpegconverter.streams.AudioStream; import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.streams.StreamObject; 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 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 java.util.List;
/** /**
@ -19,29 +26,35 @@ public class AudioConverter extends AbstractConverter {
* @param ffmpegPath <p>Path/command to ffmpeg.</p> * @param ffmpegPath <p>Path/command to ffmpeg.</p>
* @param newExtension <p>The extension of the new file.</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); super(newExtension);
this.ffprobePath = ffprobePath; this.ffprobePath = ffprobePath;
this.ffmpegPath = ffmpegPath; this.ffmpegPath = ffmpegPath;
} }
@Override @Override
public String[] generateConversionCommand(String executable, File file, List<StreamObject> streams, String outFile) { @Nullable
List<String> command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, file.getName()); 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) { 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 //Gets the first audio stream from the file and adds it to the output file
AudioStream audioStream = getFirstAudioStream(filterStreamsByType(streams, AudioStream.class)); modules.add(new NthAudioStreamModule(probeResult.getAudioStreams(), 0));
FFMpegHelper.addAudioStream(command, audioStream, false); modules.add(new SetOutputFileModule(outFile));
command.add(outFile);
return command.toArray(new String[0]); new ModuleExecutor(command, modules).execute();
return command;
} }
@Override @Override
public String[] getValidFormats() { @NotNull
public List<String> getValidFormats() {
return audioFormats; return audioFormats;
} }
} }

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -1,6 +1,9 @@
package net.knarcraft.ffmpegconverter.converter; 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.File;
import java.io.IOException; import java.io.IOException;
@ -9,7 +12,15 @@ import java.util.List;
/** /**
* This interface describes a file converter * 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 * Converts the given file
@ -17,16 +28,18 @@ interface Converter {
* @param file <p>The file to convert.</p> * @param file <p>The file to convert.</p>
* @throws IOException <p>If the file cannot be converted.</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. * Generates a command for a ProcessBuilder.
* *
* @param executable <p>The executable file for ffmpeg.</p> * @param executable <p>The executable file for ffmpeg</p>
* @param file <p>The input file.</p> * @param probeResult <p>The result of probing the input file</p>
* @param streams <p>A list of ffprobe streams.</p> * @param outFile <p>The output file</p>
* @param outFile <p>The output file.</p>
* @return <p>A list of commands</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);
} }

View File

@ -1,64 +0,0 @@
package net.knarcraft.ffmpegconverter.converter;
import net.knarcraft.ffmpegconverter.utility.FileUtil;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* The ConverterProfiles class is responsible for loading and retrieving settings for a converter
*/
public class ConverterProfiles {
private Map<String, Map<String, String>> loadedProfiles;
/**
* Instantiates a new converter profiles object
*/
public ConverterProfiles() {
loadedProfiles = new HashMap<>();
try {
loadProfiles();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Gets all available converter profiles as a set
* @return <p>A set of all loaded converter profiles.</p>
*/
public Set<String> getProfiles() {
return loadedProfiles.keySet();
}
/**
* Gets all profile settings for the given converter profile
* @param profileName <p>The name of the converter profile to get settings for.</p>
* @return <p>Settings for the converter profile.</p>
*/
public Map<String, String> getProfileSettings(String profileName) {
return loadedProfiles.get(profileName);
}
/**
* Loads all converter profiles
* @throws IOException <p>If unable to read the converter profiles file.</p>
*/
private void loadProfiles() throws IOException {
String[] profiles = FileUtil.readFileLines("converter_profiles.txt");
for (String profile : profiles) {
Map<String, String> profileSettings = new HashMap<>();
String[] settings = profile.split("\\|");
String profileName = settings[0];
for (int i = 1; i < settings.length; i++) {
String[] settingParts = settings[i].split(":");
profileSettings.put(settingParts[0], settingParts[1]);
}
loadedProfiles.put(profileName, profileSettings);
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -1,9 +1,20 @@
package net.knarcraft.ffmpegconverter.converter; 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.streams.StreamObject;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper; 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 java.util.List;
/** /**
@ -25,24 +36,31 @@ public class VideoConverter extends AbstractConverter {
} }
@Override @Override
public String[] generateConversionCommand(String executable, File file, List<StreamObject> streams, String outFile) { @Nullable
List<String> command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, file.getName()); 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) { if (this.debug) {
FFMpegHelper.addDebugArguments(command, 50, 120); modules.add(new DebugModule());
} }
//Add all streams without re-encoding //Add all streams without re-encoding
command.add("-map"); modules.add(new MapAllModule<>(streams));
command.add("0"); modules.add(new HardwareDecodeModule());
command.add("-c"); modules.add(new CopyAllModule());
command.add("copy");
command.add(outFile); modules.add(new SetOutputFileModule(outFile));
return command.toArray(new String[0]);
new ModuleExecutor(command, modules).execute();
return command;
} }
@Override @Override
public String[] getValidFormats() { @NotNull
public List<String> getValidFormats() {
return videoFormats; return videoFormats;
} }

View File

@ -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;
}
}

View File

@ -1,14 +1,29 @@
package net.knarcraft.ffmpegconverter.converter; package net.knarcraft.ffmpegconverter.converter;
import net.knarcraft.ffmpegconverter.streams.AudioStream; import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.streams.StreamObject; 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.SubtitleStream;
import net.knarcraft.ffmpegconverter.streams.VideoStream; import net.knarcraft.ffmpegconverter.streams.VideoStream;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper; 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 java.util.List;
import static net.knarcraft.ffmpegconverter.utility.FFMpegHelper.getNthSteam;
/**
* A simple converter for web-video
*/
public class WebVideoConverter extends AbstractConverter { public class WebVideoConverter extends AbstractConverter {
/** /**
@ -25,29 +40,41 @@ public class WebVideoConverter extends AbstractConverter {
} }
@Override @Override
public String[] getValidFormats() { @NotNull
public List<String> getValidFormats() {
return videoFormats; return videoFormats;
} }
@Override @Override
public String[] generateConversionCommand(String executable, File file, List<StreamObject> streams, String outFile) { @Nullable
List<String> command = FFMpegHelper.getFFMpegWebVideoCommand(executable, file.getName()); 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) { if (this.debug) {
FFMpegHelper.addDebugArguments(command, 50, 120); modules.add(new DebugModule());
} }
//Get first streams from the file //Get first streams from the file
SubtitleStream subtitleStream = getFirstSubtitleStream(filterStreamsByType(streams, SubtitleStream.class)); SubtitleStream subtitleStream = getNthSteam(probeResult.getSubtitleStreams(), 0);
VideoStream videoStream = getFirstVideoStream(filterStreamsByType(streams, VideoStream.class)); VideoStream videoStream = getNthSteam(probeResult.getVideoStreams(), 0);
AudioStream audioStream = getFirstAudioStream(filterStreamsByType(streams, AudioStream.class));
//Add streams to output if (videoStream == null) {
FFMpegHelper.addSubtitleAndVideoStream(command, subtitleStream, videoStream, file); throw new IllegalArgumentException("The selected video stream does not exist.");
if (audioStream != null) {
FFMpegHelper.addAudioStream(command, audioStream, true);
} }
command.add(outFile); if (subtitleStream != null) {
return command.toArray(new String[0]); 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;
} }
} }

View File

@ -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);
}

View File

@ -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");
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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.");
}
}
}

View File

@ -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.");
}
}
}

View File

@ -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.");
}
}
}

View File

@ -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);
}
}

View File

@ -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));
}
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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);
}
}

View File

@ -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");
}
}
}

View File

@ -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);
}
}

View File

@ -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()));
}
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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");
}
}
}

View File

@ -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");
}
}
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -1,6 +1,7 @@
package net.knarcraft.ffmpegconverter.parser; package net.knarcraft.ffmpegconverter.parser;
import net.knarcraft.ffmpegconverter.utility.ListUtil; import net.knarcraft.ffmpegconverter.utility.ListUtil;
import org.jetbrains.annotations.NotNull;
/** /**
* A class representing a command argument * A class representing a command argument
@ -8,18 +9,20 @@ import net.knarcraft.ffmpegconverter.utility.ListUtil;
public class ConverterArgument { public class ConverterArgument {
private final String name; private final String name;
private final String shorthand; private final char shorthand;
private final boolean valueRequired; private final boolean valueRequired;
private final ConverterArgumentValue valueType; private final ConverterArgumentValueType valueType;
/** /**
* Instantiates a converter argument * 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 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 valueRequired <p>Whether the argument must be followed by a valid value.</p>
* @param valueType <p>The type of value the argument requires.</p> * @param valueType <p>The type of value the argument requires.</p>
*/ */
public ConverterArgument(String name, String shorthand, boolean valueRequired, ConverterArgumentValue valueType) { public ConverterArgument(@NotNull String name, char shorthand, boolean valueRequired,
@NotNull ConverterArgumentValueType valueType) {
this.name = name; this.name = name;
this.shorthand = shorthand; this.shorthand = shorthand;
this.valueRequired = valueRequired; this.valueRequired = valueRequired;
@ -28,6 +31,7 @@ public class ConverterArgument {
/** /**
* Gets the argument name * Gets the argument name
*
* @return <p>The argument name.</p> * @return <p>The argument name.</p>
*/ */
public String getName() { public String getName() {
@ -36,14 +40,16 @@ public class ConverterArgument {
/** /**
* Gets the argument shorthand * Gets the argument shorthand
*
* @return <p>The argument shorthand</p> * @return <p>The argument shorthand</p>
*/ */
public String getShorthand() { public char getShorthand() {
return shorthand; return shorthand;
} }
/** /**
* Gets whether the argument requires a value * Gets whether the argument requires a value
*
* @return <p>Whether the argument requires a value.</p> * @return <p>Whether the argument requires a value.</p>
*/ */
public boolean isValueRequired() { public boolean isValueRequired() {
@ -56,8 +62,8 @@ public class ConverterArgument {
* @param value <p>The value to test.</p> * @param value <p>The value to test.</p>
* @return <p>True if the argument is valid. False otherwise.</p> * @return <p>True if the argument is valid. False otherwise.</p>
*/ */
public boolean testArgumentValue(String value) { public boolean testArgumentValue(@NotNull String value) {
if (value.length() == 0) { if (value.isEmpty()) {
return !valueRequired; return !valueRequired;
} }
if (valueRequired && value.startsWith("-")) { if (valueRequired && value.startsWith("-")) {
@ -73,9 +79,14 @@ public class ConverterArgument {
case STRING: case STRING:
return true; return true;
case INT: case INT:
int ignored = Integer.parseInt(value); try {
return true; Integer.parseInt(value);
return true;
} catch (NumberFormatException exception) {
return false;
}
} }
return false; return false;
} }
} }

View File

@ -1,8 +0,0 @@
package net.knarcraft.ffmpegconverter.parser;
public enum ConverterArgumentValue {
BOOLEAN,
COMMA_SEPARATED_LIST,
STRING,
INT
}

View File

@ -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
}

View File

@ -1,46 +0,0 @@
package net.knarcraft.ffmpegconverter.parser;
import net.knarcraft.ffmpegconverter.utility.FileUtil;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
class ConverterArgumentsLoader {
private List<ConverterArgument> converterArguments;
/**
* Instantiates a new converter arguments loader and loads converter arguments
*/
ConverterArgumentsLoader() {
this.converterArguments = new ArrayList<>();
try {
loadConverterArguments();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Gets loaded converter arguments
* @return <p>The loaded converter arguments.</p>
*/
List<ConverterArgument> getConverterArguments() {
return new ArrayList<>(this.converterArguments);
}
/**
* Loads all converter arguments
* @throws IOException <p>If unable to read the converter argument file.</p>
*/
private void loadConverterArguments() throws IOException {
String[] arguments = FileUtil.readFileLines("converter_arguments.txt");
for (String argument : arguments) {
String[] argumentFields = argument.split("\\|");
converterArguments.add(new ConverterArgument(argumentFields[0], argumentFields[1], Boolean.parseBoolean(argumentFields[2]),
ConverterArgumentValue.valueOf(argumentFields[3])));
}
}
}

View File

@ -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,
}

View File

@ -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
}

View File

@ -1,15 +1,44 @@
package net.knarcraft.ffmpegconverter.streams; 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 * An abstract implementation of a stream object implementing common methods
*/ */
public abstract class AbstractStream implements StreamObject { public abstract class AbstractStream implements StreamObject {
int absoluteIndex;
int relativeIndex; protected final int inputIndex;
String codecName; protected final int absoluteIndex;
String language; 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 @Override
@NotNull
public String getCodecName() { public String getCodecName() {
return this.codecName; return this.codecName;
} }
@ -25,8 +54,64 @@ public abstract class AbstractStream implements StreamObject {
} }
@Override @Override
@NotNull
public String getLanguage() { public String getLanguage() {
return this.language; 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;
}
} }

View File

@ -1,30 +1,38 @@
package net.knarcraft.ffmpegconverter.streams; 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 { public class AudioStream extends AbstractStream implements StreamObject {
private final int channels; private final int channels;
private final String title; private final boolean isSpecialAudio;
/** /**
* Instantiates a new audio stream * Instantiates a new audio stream
* *
* @param codecName <p>The codec of the audio stream.</p> * @param streamInfo <p>All info about the stream</p>
* @param absoluteIndex <p>The index of the audio 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> * @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>
*/ */
public AudioStream(String codecName, int absoluteIndex, int relativeIndex, String language, String title, public AudioStream(@NotNull Map<StreamTag, String> streamInfo, int inputIndex, int relativeIndex) {
int channels) { super(streamInfo, inputIndex, relativeIndex);
this.codecName = codecName; this.channels = ValueParsingHelper.parseInt(streamInfo.get(StreamTag.CHANNELS), 0);
this.absoluteIndex = absoluteIndex; this.isSpecialAudio = checkIfIsSpecialAudio();
this.language = language; }
this.title = title;
this.relativeIndex = relativeIndex; /**
this.channels = channels; * 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; return this.channels;
} }
/** @Override
* Gets the title of the audio stream public char streamTypeCharacter() {
* return 'a';
* @return <p>The title of the audio stream.</p>
*/
public String getTitle() {
return this.title;
} }
/**
* Checks whether this audio stream is a special audio stream
*
* @return <p>True if this is a special audio stream</p>
*/
private boolean checkIfIsSpecialAudio() {
String titleLowercase = getTitle().toLowerCase().trim();
return titleLowercase.matches(".*audio description.*");
}
} }

View File

@ -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 '?';
}
}

View File

@ -1,5 +1,11 @@
package net.knarcraft.ffmpegconverter.streams; package net.knarcraft.ffmpegconverter.streams;
import org.jetbrains.annotations.NotNull;
/**
* An object describing a generic video file stream
*/
@SuppressWarnings("unused")
public interface StreamObject { public interface StreamObject {
/** /**
@ -7,6 +13,7 @@ public interface StreamObject {
* *
* @return <p>Codec name.</p> * @return <p>Codec name.</p>
*/ */
@NotNull
String getCodecName(); String getCodecName();
/** /**
@ -16,6 +23,15 @@ public interface StreamObject {
*/ */
int getAbsoluteIndex(); 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) * Gets the relative index of a stream object (kth element of codec type)
* *
@ -24,10 +40,50 @@ public interface StreamObject {
int getRelativeIndex(); 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(); 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);
} }

View File

@ -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;
}
}

View File

@ -1,52 +1,30 @@
package net.knarcraft.ffmpegconverter.streams; 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 * An object representation of a subtitle stream in a media file
*/ */
public class SubtitleStream extends AbstractStream implements StreamObject { public class SubtitleStream extends AbstractStream implements StreamObject {
final private String title;
final private String file; private final boolean isFullSubtitle;
final private boolean isFullSubtitle; private final boolean isImageSubtitle;
final private boolean isImageSubtitle;
/** /**
* Instantiates a new subtitle stream * Instantiates a new subtitle stream
* *
* @param codecName <p>The name of the codec for the subtitle stream.</p> * @param streamInfo <p>All info about the stream</p>
* @param absoluteIndex <p>The index of the subtitle 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> * @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>
*/ */
public SubtitleStream(String codecName, int absoluteIndex, int relativeIndex, String language, String title, public SubtitleStream(@NotNull Map<StreamTag, String> streamInfo, int inputIndex, int relativeIndex) {
String file) { super(streamInfo, inputIndex, relativeIndex);
this.codecName = codecName; this.isFullSubtitle = checkIfIsFullSubtitle();
this.absoluteIndex = absoluteIndex; this.isImageSubtitle = codecName != null &&
this.language = language; (getCodecName().equals("hdmv_pgs_subtitle") || getCodecName().equals("dvd_subtitle"));
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>
*/
public 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;
} }
/** /**
@ -54,7 +32,7 @@ public class SubtitleStream extends AbstractStream implements StreamObject {
* *
* @return <p>Whether the subtitles is an image subtitle.</p> * @return <p>Whether the subtitles is an image subtitle.</p>
*/ */
public boolean getIsImageSubtitle() { public boolean isImageSubtitle() {
return this.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> * @return <p>Whether the subtitle is a full subtitle.</p>
*/ */
public boolean getIsFullSubtitle() { public boolean isFullSubtitle() {
return this.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) * Checks whether the subtitle translates everything (as opposed to just songs and signs)
* *
* @return <p>True if the subtitle translates everything.</p> * @return <p>True if the subtitle translates everything.</p>
*/ */
private boolean isFullSubtitle() { private boolean checkIfIsFullSubtitle() {
if (getTitle() == null) { return !SubtitleHelper.isSongsSignsSubtitle(this.getTitle());
return false;
}
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;
}
} }

View File

@ -1,27 +1,29 @@
package net.knarcraft.ffmpegconverter.streams; 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 * An object representation of a video stream in a media file
*/ */
public class VideoStream extends AbstractStream implements StreamObject { public class VideoStream extends AbstractStream implements StreamObject {
private final int width; private final int width;
private final int height; private final int height;
/** /**
* Instantiates a new video stream * Instantiates a new video stream
* *
* @param codec <p>The name of the codec for the video stream.</p> * @param streamInfo <p>All info about the stream</p>
* @param absoluteIndex <p>The index of the video 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> * @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>
*/ */
public VideoStream(String codec, int absoluteIndex, int relativeIndex, int width, int height) { public VideoStream(@NotNull Map<StreamTag, String> streamInfo, int inputIndex, int relativeIndex) {
this.codecName = codec; super(streamInfo, inputIndex, relativeIndex);
this.absoluteIndex = absoluteIndex; this.width = ValueParsingHelper.parseInt(streamInfo.get(StreamTag.WIDTH), -1);
this.relativeIndex = relativeIndex; this.height = ValueParsingHelper.parseInt(streamInfo.get(StreamTag.HEIGHT), -1);
this.width = width;
this.height = height;
} }
/** /**
@ -41,4 +43,10 @@ public class VideoStream extends AbstractStream implements StreamObject {
public int getHeight() { public int getHeight() {
return this.height; return this.height;
} }
@Override
public char streamTypeCharacter() {
return 'v';
}
} }

View File

@ -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();
}
}
}

View File

@ -1,9 +1,18 @@
package net.knarcraft.ffmpegconverter.utility; 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.AudioStream;
import net.knarcraft.ffmpegconverter.streams.OtherStream;
import net.knarcraft.ffmpegconverter.streams.StreamObject; import net.knarcraft.ffmpegconverter.streams.StreamObject;
import net.knarcraft.ffmpegconverter.streams.StreamTag;
import net.knarcraft.ffmpegconverter.streams.SubtitleStream; import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
import net.knarcraft.ffmpegconverter.streams.VideoStream; import net.knarcraft.ffmpegconverter.streams.VideoStream;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.File; import java.io.File;
@ -11,7 +20,9 @@ import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* A class which helps with ffmpeg probing and converting * 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 static final String PROBE_SPLIT_CHARACTER = "øæåÆØå";
private FFMpegHelper() { private FFMpegHelper() {
} }
/** /**
* Gets streams from a file * Gets streams from a file
* *
* @param ffprobePath <p>The path/command to ffprobe.</p> * @param ffprobePath <p>The path/command to ffprobe</p>
* @param file <p>The file to probe.</p> * @param file <p>The file to probe</p>
* @return <p>A list of StreamObjects.</p> * @param subtitleFormats <p>The extensions to accept for external subtitles</p>
* @throws IOException <p>If the process can't be readProcess.</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 { @NotNull
return parseStreams(ffprobePath, probeForStreams(ffprobePath, file), file); 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 * 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 executable <p>The executable to use (ffmpeg/ffprobe)</p>
* @param fileName <p>The name of the file to execute on.</p> * @param files <p>The files to execute on</p>
* @return <p>A base list of ffmpeg commands for converting a video for web</p> * @return <p>A FFMPEG command for web-playable video</p>
*/ */
public static List<String> getFFMpegWebVideoCommand(String executable, String fileName) { @NotNull
List<String> command = getFFMpegGeneralFileCommand(executable, fileName); public static FFMpegCommand getFFMpegWebVideoCommand(@NotNull String executable, @NotNull List<File> files) {
command.add("-vcodec"); FFMpegCommand command = getFFMpegGeneralFileCommand(executable, files);
command.add("h264"); command.addOutputFileOption("-vcodec", "h264");
command.add("-pix_fmt"); command.addOutputFileOption("-pix_fmt", "yuv420p");
command.add("yuv420p"); command.addOutputFileOption("-ar", "48000");
command.add("-ar"); command.addOutputFileOption("-movflags", "+faststart");
command.add("48000"); command.addOutputFileOption("-map_metadata", "0");
command.add("-movflags"); command.addOutputFileOption("-movflags", "+use_metadata_tags");
command.add("+faststart");
return command; return command;
} }
/** /**
* Creates a list containing command line arguments for a general file * Creates a list containing command line arguments for a general file
* *
* @param executable <p>The executable to use (ffmpeg/ffprobe).</p> * @param executable <p>The executable to use (ffmpeg/ffprobe)</p>
* @param fileName <p>The name of the file to execute on.</p> * @param files <p>The files to execute on</p>
* @return <p>A base list of ffmpeg commands for converting a file.</p> * @return <p>A basic FFMPEG command</p>
*/ */
public static List<String> getFFMpegGeneralFileCommand(String executable, String fileName) { @NotNull
List<String> command = new ArrayList<>(); public static FFMpegCommand getFFMpegGeneralFileCommand(@NotNull String executable, @NotNull List<File> files) {
command.add(executable); FFMpegCommand command = new FFMpegCommand(executable);
command.add("-nostdin"); command.addGlobalOption("-nostdin");
command.add("-i"); for (File file : files) {
command.add(fileName); command.addInputFile(file.getName());
}
command.addOutputFileOption("-map_metadata", "0");
command.addOutputFileOption("-movflags", "+use_metadata_tags");
return command; return command;
} }
/**
* 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);
}
/** /**
* Starts and prints output of a process * Starts and prints output of a process
* *
* @param processBuilder <p>The process to run.</p> * @param processBuilder <p>The process to run</p>
* @param folder <p>The folder the process should run in.</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 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> * @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> * @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) @NotNull
throws IOException { 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 //Give the user information about what's about to happen
OutputUtil.print("Command to be run: "); OutputUtil.print("Command to be run: ");
OutputUtil.println(processBuilder.command().toString()); OutputUtil.println(processBuilder.command().toString());
//Set directory and error stream //Set directory and error stream
processBuilder.directory(folder); if (folder != null) {
processBuilder.directory(folder);
}
processBuilder.redirectErrorStream(true); processBuilder.redirectErrorStream(true);
Process process = processBuilder.start(); Process process = processBuilder.start();
@ -109,90 +116,75 @@ public final class FFMpegHelper {
StringBuilder output = new StringBuilder(); StringBuilder output = new StringBuilder();
while (process.isAlive()) { while (process.isAlive()) {
String read = readProcess(processReader, spacer); String read = readProcess(processReader, spacer);
if (!read.equals("")) { if (read.isEmpty()) {
if (write) { continue;
OutputUtil.println(read); }
} else {
OutputUtil.printDebug(read); if (write) {
output.append(read); OutputUtil.println(read);
} } else {
OutputUtil.printDebug(read);
output.append(read);
} }
} }
OutputUtil.println("Process finished."); try {
return output.toString(); int exitCode = process.waitFor();
} OutputUtil.println("Process finished with exit code: " + exitCode);
return new ProcessResult(exitCode, output.toString());
/** } catch (InterruptedException e) {
* Adds audio to a command return new ProcessResult(1, output.toString());
*
* @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 * Adds arguments for converting a file to h264 using hardware acceleration
* *
* @param command <p>The list containing the rest of the command.</p> * @param command <p>The command to add the arguments to</p>
* @param subtitleStream <p>The subtitle stream to be used.</p> * @param quality <p>The quality to encode. 0 = best, 51 = worst.</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, public static void addH264HardwareEncoding(@NotNull FFMpegCommand command, int quality) {
VideoStream videoStream, File file) { command.addOutputFileOption("-codec:v", "h264_nvenc");
//No appropriate subtitle was found. Just add the video stream. command.addOutputFileOption("-profile", "high");
if (subtitleStream == null) { command.addOutputFileOption("-preset", "p7");
addVideoStream(command, videoStream); command.addOutputFileOption("-crf", String.valueOf(quality));
return; }
}
//Add the correct command arguments depending on the subtitle type /**
if (!subtitleStream.getIsImageSubtitle()) { * Adds arguments for converting a file to h265 using hardware acceleration
addSubtitle(command, subtitleStream, videoStream); *
} else if (file.getName().equals(subtitleStream.getFile())) { * @param command <p>The command to add the arguments to</p>
addInternalImageSubtitle(command, subtitleStream, videoStream); * @param quality <p>The quality to encode. 0 = best, 51 = worst.</p>
} else { */
addExternalImageSubtitle(command, subtitleStream, videoStream); 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);
} }
} }
/** /**
* Adds video mapping to a command * Maps the given stream to the given FFMPEG command's output
* *
* @param command <p>The list containing the rest of the command.</p> * @param command <p>The command to map the stream to</p>
* @param videoStream <p>The video stream to be used.</p> * @param stream <p>The stream to map</p>
*/ */
private static void addVideoStream(List<String> command, VideoStream videoStream) { public static void mapStream(@NotNull FFMpegCommand command, @NotNull StreamObject stream) {
command.add("-map"); command.addOutputFileOption("-map", String.format("%d:%d", stream.getInputIndex(),
command.add(String.format("0:%d", videoStream.getAbsoluteIndex())); stream.getAbsoluteIndex()));
}
/**
* Adds subtitle commands to a command list
*
* @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>
*/
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);
} }
/** /**
@ -201,7 +193,8 @@ public final class FFMpegHelper {
* @param fileName <p>The filename to escape.</p> * @param fileName <p>The filename to escape.</p>
* @return <p>A filename with known special characters escaped.</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("\\\\", "\\\\\\\\\\\\\\\\") return fileName.replaceAll("\\\\", "\\\\\\\\\\\\\\\\")
.replaceAll("'", "'\\\\\\\\\\\\\''") .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 streams <p>A list of streams</p>
* @param subtitleStream <p>The subtitle stream to add.</p> * @param n <p>The index of the audio stream to get</p>
* @param videoStream <p>The video stream to burn the subtitle into.</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, public static <G extends StreamObject> G getNthSteam(@NotNull List<G> streams, int n) {
VideoStream videoStream) { if (n < 0) {
command.add("-filter_complex"); throw new IllegalArgumentException("N cannot be negative!");
String filter = String.format("[0:v:%d][0:%d]overlay", videoStream.getAbsoluteIndex(), }
subtitleStream.getAbsoluteIndex()); G stream = null;
command.add(filter); if (streams.size() > n) {
} stream = streams.get(n);
} else if (!streams.isEmpty()) {
/** stream = streams.get(0);
* Adds external image subtitle commands to a command list }
* return stream;
* @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");
} }
/** /**
@ -252,80 +231,200 @@ public final class FFMpegHelper {
* @return <p>A list of streams.</p> * @return <p>A list of streams.</p>
* @throws IOException <p>If something goes wrong while probing.</p> * @throws IOException <p>If something goes wrong while probing.</p>
*/ */
private static String[] probeForStreams(String ffprobePath, File file) throws IOException { @NotNull
ProcessBuilder processBuilder = new ProcessBuilder( private static List<String> probeForStreams(@NotNull String ffprobePath, @NotNull File file) throws IOException {
ffprobePath, FFMpegCommand probeCommand = new FFMpegCommand(ffprobePath);
"-v", probeCommand.addGlobalOption("-v", "error", "-show_streams");
"error", probeCommand.addInputFile(file.toString());
"-show_entries",
"stream_tags=language,title:stream=index,codec_name,codec_type,channels,codec_type,width,height", ProcessBuilder processBuilder = new ProcessBuilder(probeCommand.getResult());
file.toString() ProcessResult result = runProcess(processBuilder, file.getParentFile(), PROBE_SPLIT_CHARACTER, false);
); if (result.exitCode() != 0) {
String result = runProcess(processBuilder, file.getParentFile(), PROBE_SPLIT_CHARACTER, false); throw new IllegalArgumentException("File probe failed with code " + result.exitCode());
return StringUtil.stringBetween(result, "[STREAM]", "[/STREAM]"); }
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 * Takes a list of all streams and parses each stream into one of three objects
* *
* @param streams <p>A list of all streams for the current file.</p> * @param ffprobePath <p>The path to the ffprobe executable</p>
* @param file <p>The file currently being converted.</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> * @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<>(); List<StreamObject> parsedStreams = new ArrayList<>();
int relativeAudioIndex = 0; int relativeAudioIndex = 0;
int relativeVideoIndex = 0; int relativeVideoIndex = 0;
int relativeSubtitleIndex = 0; int relativeSubtitleIndex = 0;
for (String stream : streams) { for (String stream : streams) {
String[] streamParts = stream.split(PROBE_SPLIT_CHARACTER); String[] streamParts = stream.split(PROBE_SPLIT_CHARACTER);
if (stream.contains("codec_type=video")) { Map<StreamTag, String> streamInfo = getStreamInfo(streamParts);
parsedStreams.add(parseVideoStream(streamParts, relativeVideoIndex++)); StreamType streamType = getStreamType(streamInfo);
} else if (stream.contains("codec_type=audio")) {
parsedStreams.add(parseAudioStream(streamParts, relativeAudioIndex++)); switch (streamType) {
} else if (stream.contains("codec_type=subtitle")) { case VIDEO -> parsedStreams.add(new VideoStream(streamInfo, 0, relativeVideoIndex++));
parsedStreams.add(parseSubtitleStream(streamParts, relativeSubtitleIndex++, file.getName())); 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; 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 streamInfo <p>The information describing the stream</p>
* @param directory <p>The directory containing the file.</p> * @return <p>The type of the stream</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>
*/ */
private static List<StreamObject> getExternalSubtitles(String ffprobePath, File directory, String convertingFile) @NotNull
throws IOException { private static StreamType getStreamType(@NotNull Map<StreamTag, String> streamInfo) {
List<StreamObject> parsedStreams = new ArrayList<>(); 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 //Find all files in the same directory with external subtitle formats
String[] subtitleFormats = FileUtil.readFileLines("subtitle_formats.txt"); File[] files = FileUtil.listFilesRecursive(directory, formats, 1);
File[] subtitleFiles = FileUtil.listFilesRecursive(directory, subtitleFormats, 1);
//Return early if no files were found //Return early if no files were found
if (subtitleFiles == null) { if (files == null) {
return parsedStreams; return;
} }
String fileTitle = FileUtil.stripExtension(convertingFile); 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 //Finds the files which are subtitles probably belonging to the file
subtitleFilesList = ListUtil.getMatching(subtitleFilesList, filesList = ListUtil.getMatching(filesList, (file) -> file.getName().contains(fileTitle));
(subtitleFile) -> subtitleFile.getName().contains(fileTitle));
for (File subtitleFile : subtitleFilesList) { for (File file : filesList) {
int inputIndex = streamProbeResult.parsedFiles().size();
streamProbeResult.parsedFiles().add(file);
//Probe the files and add them to the result list //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) { for (String stream : streams) {
String[] streamParts = stream.split(PROBE_SPLIT_CHARACTER); 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> * @return <p>The output from the readProcess.</p>
* @throws IOException <p>On reader failure.</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; String line;
StringBuilder text = new StringBuilder(); 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); text.append(line).append(spacer);
} }
return text.toString().trim(); 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 ffmpegPath <p>The path to ffmpeg's executable</p>
* @param relativeIndex <p>The relative index of the video stream.</p> * @return <p>The available hardware acceleration methods</p>
* @return <p>A SubtitleStream object.</p> * @throws IOException <p>If the process fails</p>
* @throws NumberFormatException <p>If codec index contains a non-numeric value.</p>
*/ */
private static VideoStream parseVideoStream(String[] streamParts, int relativeIndex) throws NumberFormatException { @NotNull
String codec = null; public static List<String> getHWAcceleration(@NotNull String ffmpegPath) throws IOException {
int absoluteIndex = -1; FFMpegCommand probeCommand = new FFMpegCommand(ffmpegPath);
int width = -1; probeCommand.addGlobalOption("-v", "error", "-hwaccels");
int height = -1;
for (String streamPart : streamParts) { ProcessBuilder processBuilder = new ProcessBuilder(probeCommand.getResult());
if (streamPart.startsWith("codec_name=")) { ProcessResult result = runProcess(processBuilder, null, PROBE_SPLIT_CHARACTER, false);
codec = streamPart.replace("codec_name=", ""); return List.of(result.output().split(PROBE_SPLIT_CHARACTER));
} 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);
} }
/**
* 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);
}
} }

View 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;
}
}

View File

@ -1,10 +1,10 @@
package net.knarcraft.ffmpegconverter.utility; 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.File;
import java.io.IOException; import java.util.List;
import java.io.InputStream;
import java.io.InputStreamReader;
/** /**
* A class which helps with file handling * A class which helps with file handling
@ -12,6 +12,7 @@ import java.io.InputStreamReader;
public final class FileUtil { public final class FileUtil {
private FileUtil() { private FileUtil() {
} }
/** /**
@ -22,19 +23,10 @@ public final class FileUtil {
* @param outExtension <p>The extension of the output file.</p> * @param outExtension <p>The extension of the output file.</p>
* @return <p>A file name with the new extension and without any collisions.</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 + return FileUtil.getNonCollidingFilename(folder.getAbsolutePath() + File.separator +
FileUtil.stripExtension(file) + "." + outExtension, outExtension); FileUtil.stripExtension(file.getName()) + "." + 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('.'));
} }
/** /**
@ -44,14 +36,16 @@ public final class FileUtil {
* @param maxRecursions <p>Maximum number of recursions</p> * @param maxRecursions <p>Maximum number of recursions</p>
* @return A list of files * @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 //Return if the target depth has been reached
if (maxRecursions == 0) { if (maxRecursions == 0) {
return null; return null;
} }
//Get a list of all files which are folders and has one of the extensions specified //Get a list of all files which are folders and has one of the extensions specified
File[] foundFiles = folder.listFiles((file) -> file.isFile() && 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 //Return if recursion is finished
if (maxRecursions == 1) { if (maxRecursions == 1) {
return foundFiles; return foundFiles;
@ -77,36 +71,6 @@ public final class FileUtil {
return foundFiles; 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 * 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> * @param extension <p>The extension of the target file.</p>
* @return <p>A filename guaranteed not to collide with other files.</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); File newFile = new File(targetPath);
String fileName = stripExtension(targetPath); String fileName = stripExtension(targetPath).replaceAll("\\([0-9]+\\)$", "");
int i = 1; int i = 1;
while (newFile.exists()) { while (newFile.exists()) {
newFile = new File(fileName + "(" + i++ + ")" + "." + extension); 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> * @param file <p>The filename to check</p>
* @return <p>A filename.</p> * @return <p>The file's extension</p>
*/ */
private static String stripExtension(File file) { @NotNull
return file.getName().substring(0, file.getName().lastIndexOf('.')); 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('.'));
} }
} }

View File

@ -1,5 +1,7 @@
package net.knarcraft.ffmpegconverter.utility; package net.knarcraft.ffmpegconverter.utility;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Array; import java.lang.reflect.Array;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -11,6 +13,7 @@ import java.util.function.Predicate;
public final class ListUtil { public final class ListUtil {
private ListUtil() { private ListUtil() {
} }
/** /**
@ -21,7 +24,8 @@ public final class ListUtil {
* @param <T> <p>The type of the two lists.</p> * @param <T> <p>The type of the two lists.</p>
* @return <p>A new array containing all elements from the two arrays.</p> * @return <p>A new array containing all elements from the two arrays.</p>
*/ */
public 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 listALength = listA.length;
int listBLength = listB.length; int listBLength = listB.length;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@ -39,7 +43,7 @@ public final class ListUtil {
* @param <T> <p>The type of the list.</p> * @param <T> <p>The type of the list.</p>
* @return <p>A new list containing all matching elements.</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); List<T> matching = new ArrayList<>(list);
matching.removeIf(predicate.negate()); matching.removeIf(predicate.negate());
return matching; return matching;
@ -53,7 +57,7 @@ public final class ListUtil {
* @param <T> Anything which can be stored in a list * @param <T> Anything which can be stored in a list
* @return True if at least one element fulfills the predicate * @return True if at least one element fulfills the predicate
*/ */
public 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) { for (T item : list) {
if (predicate.test(item)) { if (predicate.test(item)) {
return true; return true;
@ -68,7 +72,8 @@ public final class ListUtil {
* @param string <p>A string which may include commas.</p> * @param string <p>A string which may include commas.</p>
* @return <p>A string list.</p> * @return <p>A string list.</p>
*/ */
public static String[] getListFromCommaSeparatedString(String string) { @NotNull
public static String[] getListFromCommaSeparatedString(@NotNull String string) {
String[] result; String[] result;
if (string.contains(",")) { if (string.contains(",")) {
result = string.split(","); result = string.split(",");
@ -77,4 +82,5 @@ public final class ListUtil {
} }
return result; return result;
} }
} }

View File

@ -1,5 +1,7 @@
package net.knarcraft.ffmpegconverter.utility; package net.knarcraft.ffmpegconverter.utility;
import org.jetbrains.annotations.NotNull;
import java.io.BufferedWriter; import java.io.BufferedWriter;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
@ -8,10 +10,12 @@ import java.io.OutputStreamWriter;
* A class which helps with outputting information * A class which helps with outputting information
*/ */
public final class OutputUtil { public final class OutputUtil {
private static final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out)); private static final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));
private static boolean debug; private static boolean debug;
private OutputUtil() { private OutputUtil() {
} }
/** /**
@ -27,11 +31,14 @@ public final class OutputUtil {
* Prints something and a newline to the commandline efficiently * Prints something and a newline to the commandline efficiently
* *
* @param input <p>The text to print.</p> * @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 { public static void println(@NotNull String input) {
if (!input.equals("")) { if (!input.isEmpty()) {
writer.write(input); try {
writer.write(input);
} catch (IOException e) {
System.out.print(input);
}
} }
println(); println();
} }
@ -40,22 +47,27 @@ public final class OutputUtil {
* Prints a string * Prints a string
* *
* @param input <p>The string to print.</p> * @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) {
writer.write(input); try {
writer.flush(); writer.write(input);
writer.flush();
} catch (IOException e) {
System.out.print(input);
}
} }
/** /**
* Prints a newline * Prints a newline
*
* @throws IOException <p>If a write is not possible.</p>
*/ */
public static void println() throws IOException { public static void println() {
writer.newLine(); try {
writer.flush(); 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 * Prints a message if debug messages should be shown
* *
* @param message <p>The debug message to show.</p> * @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) { if (debug) {
print(message); println(message);
} }
} }
} }

View File

@ -1,6 +1,7 @@
package net.knarcraft.ffmpegconverter.utility; package net.knarcraft.ffmpegconverter.utility;
import net.knarcraft.ffmpegconverter.parser.ConverterArgument; import net.knarcraft.ffmpegconverter.parser.ConverterArgument;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
@ -13,26 +14,30 @@ import java.util.Map;
public final class Parser { public final class Parser {
private Parser() { private Parser() {
} }
/** /**
* This function parses the input to understandable converter instructions * This function parses the input to understandable converter instructions
* @param input <p>The input string to parse.</p> *
* @param input <p>The input string to parse.</p>
* @param validArguments <p>All arguments which are considered valid.</p> * @param validArguments <p>All arguments which are considered valid.</p>
* @return <p>A map with all parsed arguments.</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); return parse(tokenize(input), validArguments);
} }
/** /**
* This function parses command inputs into understandable converter instructions * This function parses command inputs into understandable converter instructions
* *
* @param tokens <p>A list of tokens containing all arguments</p> * @param tokens <p>A list of tokens containing all arguments</p>
* @param validArguments <p>A list of arguments which are considered valid.</p> * @param validArguments <p>A list of arguments which are considered valid.</p>
* @return <p>A map with all parsed arguments.</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<>(); Map<String, String> parsedArguments = new HashMap<>();
while (!tokens.isEmpty()) { 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 converterArguments <p>A list of all the valid arguments in existence.</p>
* @param parsedArguments <p>The map to store the parsed argument to.</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); String currentToken = tokens.remove(0);
List<ConverterArgument> foundArguments; List<ConverterArgument> foundArguments;
@ -56,8 +62,8 @@ public final class Parser {
String argumentName = currentToken.substring(2); String argumentName = currentToken.substring(2);
foundArguments = ListUtil.getMatching(converterArguments, (item) -> item.getName().equals(argumentName)); foundArguments = ListUtil.getMatching(converterArguments, (item) -> item.getName().equals(argumentName));
} else if (currentToken.startsWith("-")) { } else if (currentToken.startsWith("-")) {
String argumentShorthand = currentToken.substring(1); char argumentShorthand = currentToken.substring(1).charAt(0);
foundArguments = ListUtil.getMatching(converterArguments, (item) -> item.getShorthand().equals(argumentShorthand)); foundArguments = ListUtil.getMatching(converterArguments, (item) -> item.getShorthand() == argumentShorthand);
} else { } else {
throw new IllegalArgumentException("Unexpected value when not given an argument."); throw new IllegalArgumentException("Unexpected value when not given an argument.");
} }
@ -77,7 +83,8 @@ public final class Parser {
* @param foundArgument <p>The found argument to store.</p> * @param foundArgument <p>The found argument to store.</p>
* @param parsedArguments <p>The map to store parsed arguments to.</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; String argumentValue;
if (tokens.isEmpty()) { if (tokens.isEmpty()) {
argumentValue = ""; argumentValue = "";
@ -112,7 +119,8 @@ public final class Parser {
* @param input <p>A string.</p> * @param input <p>A string.</p>
* @return <p>A list of tokens.</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<>(); List<String> tokens = new ArrayList<>();
boolean startedQuote = false; boolean startedQuote = false;
StringBuilder currentToken = new StringBuilder(); StringBuilder currentToken = new StringBuilder();
@ -155,8 +163,8 @@ public final class Parser {
* @param index <p>The index of the read character.</p> * @param index <p>The index of the read character.</p>
* @param tokens <p>The list of processed tokens.</p> * @param tokens <p>The list of processed tokens.</p>
*/ */
private static void tokenizeNormalCharacter(StringBuilder currentToken, char character, int inputLength, int index, private static void tokenizeNormalCharacter(@NotNull StringBuilder currentToken, char character, int inputLength,
List<String> tokens) { int index, @NotNull List<String> tokens) {
currentToken.append(character); currentToken.append(character);
if (index == inputLength - 1) { if (index == inputLength - 1) {
tokens.add(currentToken.toString()); tokens.add(currentToken.toString());
@ -171,7 +179,8 @@ public final class Parser {
* @param tokens <p>The list of processed tokens.</p> * @param tokens <p>The list of processed tokens.</p>
* @return <p>True if the token is finished.</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 (!startedQuote) {
//If not inside "", a space marks the end of a parameter //If not inside "", a space marks the end of a parameter
if (isNotEmpty(currentToken)) { if (isNotEmpty(currentToken)) {
@ -190,7 +199,8 @@ public final class Parser {
* @param builder <p>The string builder to check.</p> * @param builder <p>The string builder to check.</p>
* @return <p>True if the string builder is non empty.</p> * @return <p>True if the string builder is non empty.</p>
*/ */
private static boolean isNotEmpty(StringBuilder builder) { private static boolean isNotEmpty(@NotNull StringBuilder builder) {
return !builder.toString().trim().equals(""); return !builder.toString().trim().isEmpty();
} }
} }

View File

@ -1,32 +1,42 @@
package net.knarcraft.ffmpegconverter.utility; 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 * A class which helps with operations on strings
*/ */
final class StringUtil { public final class StringUtil {
private StringUtil() { private StringUtil() {
} }
/** /**
* Finds all substrings between two substrings in a string * 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 start <p>The substring before the wanted substring.</p>
* @param end <p>The substring after 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> * @return <p>A list of all occurrences of the substring.</p>
*/ */
static String[] stringBetween(String string, String start, String end) { public static @NotNull List<String> stringBetween(@NotNull String input, @NotNull String start,
int startPosition = string.indexOf(start) + start.length(); @NotNull String end) {
//Return if the string is not found List<String> output = new ArrayList<>();
if (!string.contains(start) || string.indexOf(end, startPosition) < startPosition) { String inputString = input;
return new String[]{}; while (true) {
int startPosition = inputString.indexOf(start) + start.length();
//Return if the string is not found
if (!inputString.contains(start) || inputString.indexOf(end, startPosition) < startPosition) {
return output;
}
int endPosition = inputString.indexOf(end, startPosition);
//Get the string between the start and end string
output.add(inputString.substring(startPosition, endPosition).trim());
inputString = inputString.substring(endPosition + end.length());
} }
int endPosition = string.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));
} }
} }

View File

@ -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();
}
}

View File

@ -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);
}
}
}

View File

@ -1,4 +1,3 @@
39
3gp 3gp
aa aa
aac aac
@ -37,4 +36,6 @@ wav
wma wma
wv wv
webm webm
8svx 8svx
mka
ac3

View 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

View File

@ -1,15 +0,0 @@
14
recursions|r|true|INT
infile|i|true|STRING
audiolang|al|true|COMMA_SEPARATED_LIST
subtitlelang|sl|true|COMMA_SEPARATED_LIST
tostereo|ts|false|BOOLEAN
preventpartialsubtitles|p|false|BOOLEAN
outext|o|true|STRING
autostreamselection|as|false|BOOLEAN
burnfirstsubtitle|bs|false|BOOLEAN
burnfirstaudio|ba|false|BOOLEAN
videocodec|vc|true|STRING
pixelformat|pf|true|STRING
audiosamplingfrequency|asf|true|INT
moveheaders|m|false|BOOLEAN

View File

@ -1,5 +0,0 @@
4
Anime|audiolang:jpn,0|subtitlelang:eng,0|tostereo:true|preventpartialsubtitles:true|outext:mp4|videocodec:h264|pixelformat:yuv420p|audiosampling:48000|moveheaders:true|burnfirstaudio:true|burnfirstsubtitle:true
WebVideo|audiolang:*|subtitlelang:*|tostereo:true|preventpartialsubtitles:false|outext:|videocodec:h264|pixelformat:yuv420p|audiosampling:48000|moveheaders:true|burnfirstaudio:true|burnfirstsubtitle:true
Video|audiolang:*|subtitlelang:*|tostereo:false|preventpartialsubtitles:false|outext:|videocodec:|pixelformat:|audiosampling:|moveheaders:true|burnfirstaudio:true|burnfirstsubtitle:true
Audio|audiolang:*|subtitlelang:*|tostereo:false|preventpartialsubtitles:false|outext:|videocodec:|pixelformat:|audiosampling:|moveheaders:|burnfirstaudio:|burnfirstsubtitle:

View File

@ -1,5 +1,5 @@
4
idx idx
sub sub
srt srt
ass ass
vtt

View File

@ -1,4 +1,3 @@
31
avi avi
mpg mpg
mpeg mpeg
@ -29,4 +28,5 @@ m2v
svi svi
3g2 3g2
roq roq
nsv nsv
mpeg4

View File

@ -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"));
}
}

View File

@ -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);
}
}

View File

@ -1,14 +0,0 @@
package net.knarcraft.ffmpegconverter.parser;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class ConverterArgumentsLoaderTest {
@Test
public void loadTest() {
ConverterArgumentsLoader loader = new ConverterArgumentsLoader();
assertEquals(14, loader.getConverterArguments().size());
}
}

View File

@ -13,7 +13,7 @@ import static org.junit.Assert.assertFalse;
public class ListUtilTest { public class ListUtilTest {
private static List<Integer> matchesList; private static List<Integer> matchesList;
private static Integer[] containsList; private static List<Integer> containsList;
@BeforeClass @BeforeClass
public static void setUp() { public static void setUp() {
@ -28,7 +28,7 @@ public class ListUtilTest {
matchesList.add(19); matchesList.add(19);
matchesList.add(21); matchesList.add(21);
matchesList.add(23); matchesList.add(23);
containsList = new Integer[]{1, 3, 5, 7, 234, 23, 45}; containsList = List.of(1, 3, 5, 7, 234, 23, 45);
} }
@Test @Test

View File

@ -1,7 +1,7 @@
package net.knarcraft.ffmpegconverter.utility; package net.knarcraft.ffmpegconverter.utility;
import net.knarcraft.ffmpegconverter.parser.ConverterArgument; 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.Before;
import org.junit.Test; import org.junit.Test;
@ -18,9 +18,9 @@ public class ParserTest {
@Before @Before
public void setUp() { public void setUp() {
validArguments = new ArrayList<>(); validArguments = new ArrayList<>();
validArguments.add(new ConverterArgument("anargument", "a", true, ConverterArgumentValue.STRING)); validArguments.add(new ConverterArgument("anargument", 'a', true, ConverterArgumentValueType.STRING));
validArguments.add(new ConverterArgument("turnoff", "t", false, ConverterArgumentValue.BOOLEAN)); validArguments.add(new ConverterArgument("turnoff", 't', false, ConverterArgumentValueType.BOOLEAN));
validArguments.add(new ConverterArgument("turnon", "o", false, ConverterArgumentValue.BOOLEAN)); validArguments.add(new ConverterArgument("turnon", 'o', false, ConverterArgumentValueType.BOOLEAN));
} }
@Test @Test
@ -42,17 +42,17 @@ public class ParserTest {
assertEquals("false", parsed.get("turnon")); assertEquals("false", parsed.get("turnon"));
} }
@Test (expected = IllegalArgumentException.class) @Test(expected = IllegalArgumentException.class)
public void parseInvalidArgument() { public void parseInvalidArgument() {
Parser.parse("--someInvalidArgument hahaha", validArguments); Parser.parse("--someInvalidArgument hahaha", validArguments);
} }
@Test (expected = IllegalArgumentException.class) @Test(expected = IllegalArgumentException.class)
public void parseValueWhenExpectingArgument() { public void parseValueWhenExpectingArgument() {
Parser.parse("somevalue", validArguments); Parser.parse("somevalue", validArguments);
} }
@Test (expected = IllegalArgumentException.class) @Test(expected = IllegalArgumentException.class)
public void parseArgumentWithoutRequiredValue() { public void parseArgumentWithoutRequiredValue() {
Parser.parse("--anargument -t", validArguments); Parser.parse("--anargument -t", validArguments);
} }

View File

@ -2,27 +2,33 @@ package net.knarcraft.ffmpegconverter.utility;
import org.junit.Test; 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 { public class StringUtilTest {
@Test @Test
public void stringBetweenNoMatches() { public void stringBetweenNoMatches() {
String[] result = StringUtil.stringBetween("a test string", "[", "]"); List<String> result = StringUtil.stringBetween("a test string", "[", "]");
assertArrayEquals(new String[]{}, result); List<String> empty = new ArrayList<>();
assertEquals(empty, result);
} }
@Test @Test
public void stringBetweenOneMatch() { public void stringBetweenOneMatch() {
String[] result = StringUtil.stringBetween("a [test] string", "[", "]"); List<String> result = StringUtil.stringBetween("a [test] string", "[", "]");
assertArrayEquals(new String[]{"test"}, result); List<String> expected = List.of("test");
assertEquals(expected, result);
} }
@Test @Test
public void stringBetweenSeveralMatches() { 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"); "the string between method defined in the StringUtil class", " ", "a");
assertArrayEquals(new String[]{"long string cont", "", "lot of potenti", "m", List<String> expected = List.of("long string cont", "", "lot of potenti", "m",
"for the string between method defined in the StringUtil cl"}, result); "for the string between method defined in the StringUtil cl");
assertEquals(expected, result);
} }
} }