373 lines
14 KiB
Java
373 lines
14 KiB
Java
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.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""", 1, 13);
|
|
|
|
return switch (choice) {
|
|
case 1 -> generateWebAnimeConverter();
|
|
case 2 -> new AudioConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output extension>"));
|
|
case 3 -> new VideoConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output extension>"));
|
|
case 4 -> new WebVideoConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output extension>"));
|
|
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);
|
|
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 = -1;
|
|
int subtitleStreamIndex = -1;
|
|
int videoStreamIndex = -1;
|
|
|
|
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>
|
|
* @return <p>The non-empty choice given by the user.</p>
|
|
*/
|
|
@NotNull
|
|
private static String getChoice(@NotNull String prompt) {
|
|
OutputUtil.println(prompt);
|
|
String choice = "";
|
|
while (choice.isEmpty()) {
|
|
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(@NotNull String prompt, int min, int max) {
|
|
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;
|
|
}
|
|
|
|
}
|