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