diff --git a/src/main/java/net/knarcraft/ffmpegconverter/converter/Converter.java b/src/main/java/net/knarcraft/ffmpegconverter/converter/Converter.java index 8c0aba7..8fab2dd 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/converter/Converter.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/converter/Converter.java @@ -1,515 +1,17 @@ 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 java.io.BufferedReader; -import java.io.BufferedWriter; import java.io.File; import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.lang.reflect.Array; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.function.Predicate; /** - * Implements all methods which can be usefull for any implementation of a converter. + * This interface describes a file converter */ -public abstract class Converter { - String ffprobePath; - String ffmpegPath; - boolean DEBUG = false; - - private static final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out)); - - private static final String PROBE_SPLIT_CHARACTER = "øæåÆØå"; - - public abstract String[] getValidFormats(); - public abstract void convert(File file) throws IOException; - - final String[] AUDIO_FORMATS = new String[] {".3gp", ".aa", ".aac", ".aax", ".act", ".aiff", ".amr", ".ape", ".au", - ".awb", ".dct", ".dss", ".dvf", ".flac", ".gsm", ".iklax", ".ivs", ".m4a", ".m4b", ".m4p", ".mmf", ".mp3", - ".mpc", ".msv", ".ogg", ".oga", ".mogg", ".opus", ".ra", ".rm", ".raw", ".sln", ".tta", ".vox", ".wav", - ".wma", ".wv", ".webm", ".8svx"}; - final String[] VIDEO_FORMATS = new String[] {".avi", ".mpg", ".mpeg", ".mkv", ".wmv", ".flv", ".webm", ".3gp", - ".rmvb", ".3gpp", ".mts", ".m4v", ".mov", ".rm", ".asf", ".mp4", ".vob", ".ogv", ".drc", ".qt", ".yuv", - ".asm", ".m4p", ".mp2", ".mpe", ".mpv", ".m2v", ".svi", ".3g2", ".roq", ".nsv"}; +public interface Converter { /** - * Gets streams from a file - * @param ffprobePath

The path/command to ffprobe.

- * @param file

The file to probe.

- * @return

A list of StreamObjects.

- * @throws IOException

If the process can't be read.

+ * Converts the given file + * @param file

The file to convert.

+ * @throws IOException

If the file cannot be converted.

*/ - static List probeFile(String ffprobePath, File file) throws IOException { - ProcessBuilder builderProbe = new ProcessBuilder( - ffprobePath, - "-v", - "error", - "-show_entries", - "stream_tags=language,title:stream=index,codec_name,codec_type,channels", - file.toString() - ); - print("Probe command: "); - printl(builderProbe.command().toString()); - builderProbe.redirectErrorStream(true); - Process processProbe = builderProbe.start(); - BufferedReader readerProbe = new BufferedReader(new InputStreamReader(processProbe.getInputStream())); - StringBuilder output = new StringBuilder(); - while (processProbe.isAlive()) { - String read = read(readerProbe, PROBE_SPLIT_CHARACTER); - if (!read.equals("")) { - print(read); - output.append(read); - } - } - return parseStreams(stringBetween(output.toString(), "[STREAM]", "[/STREAM]")); - } - - /** - * Adds parentheses with an integer if the output file already exists - * @param targetPath

The path the file should ideally be saved at.

- * @param extension

The extension of the target file.

- * @return

A filename guaranteed not to collide with other files.

- */ - static String fileCollisionPrevention(String targetPath, String extension) { - File newFile = new File(targetPath); - String fileName = stripExtension(targetPath); - int i = 1; - while (newFile.exists()) { - newFile = new File(fileName + "(" + i++ + ")" + "." + extension); - } - return newFile.toString(); - } - - /** - * Starts and prints output of a process - * @param process

The process to run.

- * @param folder

The folder the process should run in.

- * @throws IOException

If the process can't be read.

- */ - static void convertProcess(ProcessBuilder process, File folder) throws IOException { - print("Command to be run: "); - printl(process.command().toString()); - process.directory(folder); - process.redirectErrorStream(true); - Process processConvert = process.start(); - BufferedReader readerConvert = new BufferedReader(new InputStreamReader(processConvert.getInputStream())); - while (processConvert.isAlive()) { - String read = read(readerConvert, "\n"); - if (!read.equals("")) { - printl(read); - } - } - printl("Process is finished."); - } - - /** - * Reads from a process reader - * @param reader

The reader of a process.

- * @return

The output from the read.

- * @throws IOException

On reader failure.

- */ - private static String read(BufferedReader reader, String spacer) throws IOException { - String line; - StringBuilder text = new StringBuilder(); - while (reader.ready() && (line = reader.readLine()) != null && !line.equals("") && !line.equals("\n")) { - text.append(line).append(spacer); - } - return text.toString().trim(); - } - - /** - * Creates a list containing all required arguments for converting a video to a web playable video - * @param executable

The executable to use (ffmpeg/ffprobe).

- * @param fileName

The name of the file to execute on.

- * @return

A base list of ffmpeg commands for converting a video for web

- */ - static List ffmpegWebVideo(String executable, String fileName) { - List command = generalFile(executable, fileName); - command.add("-vcodec"); - command.add("h264"); - command.add("-pix_fmt"); - command.add("yuv420p"); - command.add("-ar"); - command.add("48000"); - command.add("-movflags"); - command.add("+faststart"); - return command; - } - - /** - * Creates a list containing command line arguments for a general file - * @param executable

The executable to use (ffmpeg/ffprobe).

- * @param fileName

The name of the file to execute on.

- * @return

A base list of ffmpeg commands for converting a file.

- */ - static List generalFile(String executable, String fileName) { - List command = new ArrayList<>(); - command.add(executable); - command.add("-nostdin"); - command.add("-i"); - command.add(fileName); - return command; - } - - /** - * Adds debugging parameters for only converting parts of a file - * @param command

The list containing the command to run.

- * @param start

The offset before converting.

- * @param length

The offset for stopping the conversion.

- */ - static void addDebug(List command, int start, int length) { - command.add("-ss"); - command.add("" + start); - command.add("-t"); - command.add("" + length); - } - - /** - * Lists all indexes fulfilling a predicate - * @param list

A list of ffprobe indexes.

- * @return

An integer list containing just the wanted indexes

- */ - private static List listIndexes(String[] list, Predicate p) { - List indexes = new ArrayList<>(); - for (String str : list) { - if (p.test(str)) { - indexes.add(Integer.parseInt(stringBetweenSingle(str, "index=", PROBE_SPLIT_CHARACTER))); - } - } - return indexes; - } - - /** - * Tests a predicate on a list - * @param list

A list.

- * @param p

A predicate

- * @param

The type of the list and the predicate.

- * @return True if the list have an element for which the predicate is true - */ - private static boolean testPredicate(T[] list, Predicate p) { - for (T o : list) { - if (p.test(o)) { - return true; - } - } - return false; - } - - /** - * Finds all substrings between two substrings in a string - * @param string

The string containing the substrings.

- * @param start

The substring before the wanted substring.

- * @param end

The substring after the wanted substring.

- * @return

A list of all occurrences of the substring.

- */ - private static String[] stringBetween(String string, String start, String end) { - int startPos = string.indexOf(start) + start.length(); - if (!string.contains(start) || string.indexOf(end, startPos) < startPos) { - return new String[]{}; - } - int endPos = string.indexOf(end, startPos); - String outString = string.substring(startPos, endPos).trim(); - String nextString = string.substring(endPos + end.length()); - return concatenate(new String[]{outString}, stringBetween(nextString, start, end)); - } - - /** - * Finds a substring between two substrings in a string - * @param string

The string containing the substrings.

- * @param start

The substring before the wanted substring.

- * @param end

The substring after the wanted substring.

- * @return

The wanted substring.

- */ - private static String stringBetweenSingle(String string, String start, String end) { - int startPos = string.indexOf(start) + start.length(); - if (!string.contains(start) || string.indexOf(end, startPos) < startPos) { - return ""; - } - return string.substring(startPos, string.indexOf(end, startPos)); - } - - /** - * Gets filename without extension from File object - * @param file

A file object.

- * @return

A filename.

- */ - static String stripExtension(File file) { - return file.getName().substring(0, file.getName().lastIndexOf('.')); - } - - /** - * Removes the extension from a file name - * @param file

A filename.

- * @return

A filename without its extension.

- */ - static String stripExtension(String file) { - return file.substring(0, file.lastIndexOf('.')); - } - - /** - * Combines two arrays to one - * @param listA

The first array.

- * @param listB

The second array.

- * @param

The type of the two lists.

- * @return

A new array containing all elements from the two arrays.

- */ - public static T[] concatenate(T[] listA, T[] listB) { - int listALength = listA.length; - int listBLength = listB.length; - @SuppressWarnings("unchecked") - T[] resultList = (T[]) Array.newInstance(listA.getClass().getComponentType(), listALength + listBLength); - System.arraycopy(listA, 0, resultList, 0, listALength); - System.arraycopy(listB, 0, resultList, listALength, listBLength); - return resultList; - } - - /** - * Filters parsed streams into one of the stream types - * @param streams

A list of stream objects.

- * @param codecType

The codec type of the streams to select.

- * @param

The correct object type for the streams with the selected codec type.

- * @return

A potentially shorter list of streams.

- */ - static List filterStreamsByType(List streams, String codecType) { - Iterator i = streams.iterator(); - List newStreams = new ArrayList<>(); - while (i.hasNext()) { - StreamObject next = i.next(); - if (next.getCodecType().equals(codecType)) { - newStreams.add((G) next); - } - } - return newStreams; - } - - /** - * Filters and sorts audio streams according to chosen languages - * @param audioStreams

A list of audio streams.

- * @param audioLanguages

A list of languages.

- * @return

A list containing just audio tracks of chosen languages, sorted in order of languages.

- */ - static List filterAudioStreams(List audioStreams, String[] audioLanguages) { - List filtered = new ArrayList<>(); - for (String language : audioLanguages) { - for (AudioStream stream : audioStreams) { - if ((stream.getLanguage() != null && stream.getLanguage().equals(language)) || language.equals("*")) { - filtered.add(stream); - } - } - //Tries to reduce execution time from n^2 - audioStreams.removeAll(filtered); - } - return filtered; - } - - /** - * Filters and sorts subtitle streams according to chosen languages - * @param subtitleStreams

A list of subtitle streams.

- * @param subtitleLanguages

A list of languages.

- * @param preventSignsAndSongs

Whether partial subtitles should be avoided.

- * @return

A list containing just subtitles of chosen languages, sorted in order of languages.

- */ - static List filterSubtitleStreams(List subtitleStreams, String[] subtitleLanguages, - boolean preventSignsAndSongs) { - List filtered = new ArrayList<>(); - //Go through languages. Select all subtitles of the language - for (String language : subtitleLanguages) { - for (SubtitleStream stream : subtitleStreams) { - String streamLanguage = stream.getLanguage(); - if (((streamLanguage != null && streamLanguage.equals(language)) || language.equals("*")) && - (!preventSignsAndSongs || stream.getIsFullSubtitle())) { - filtered.add(stream); - } - } - //Tries to reduce execution time from n^2 - subtitleStreams.removeAll(filtered); - } - return filtered; - } - - /** - * Takes a list of all streams and parses each stream into one of three objects - * @param streams

A list of all streams for the current file.

- * @return

A list of StreamObjects.

- */ - private static List parseStreams(String[] streams) { - List parsedStreams = new ArrayList<>(); - int relativeAudioIndex = 0; - int relativeVideoIndex = 0; - int relativeSubtitleIndex = 0; - for (String stream : streams) { - String[] streamParts = stream.split(PROBE_SPLIT_CHARACTER); - if (stream.contains("codec_type=video")) { - parsedStreams.add(parseVideoStream(streamParts, relativeVideoIndex++)); - } else if (stream.contains("codec_type=audio")) { - parsedStreams.add(parseAudioStream(streamParts, relativeAudioIndex++)); - } else if (stream.contains("codec_type=subtitle")) { - parsedStreams.add(parseSubtitleStream(streamParts, relativeSubtitleIndex++)); - } - } - return parsedStreams; - } - - /** - * Parses a list of video stream parameters to a video stream object - * @param streamParts

A list of parameters belonging to an video stream.

- * @param relativeIndex

The relative index of the video stream.

- * @return

A SubtitleStream object.

- * @throws NumberFormatException

If codec index contains a non-numeric value.

- */ - private static VideoStream parseVideoStream(String[] streamParts, int relativeIndex) throws NumberFormatException { - String codec = null; - int absoluteIndex = -1; - for (String streamPart : streamParts) { - if (streamPart.contains("codec_name=")) { - codec = streamPart.replace("codec_name=", ""); - } else if (streamPart.contains("index=")) { - absoluteIndex = Integer.parseInt(streamPart.replace("index=", "")); - } - } - return new VideoStream(codec, absoluteIndex, relativeIndex); - } - - /** - * Parses a list of audio stream parameters to an audio stream object - * @param streamParts

A list of parameters belonging to an audio stream.

- * @param relativeIndex

The relative index of the audio stream.

- * @return

A SubtitleStream object.

- * @throws NumberFormatException

If codec index contains a non-numeric value.

- */ - 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.contains("codec_name=")) { - codec = streamPart.replace("codec_name=", ""); - } else if (streamPart.contains("index=")) { - absoluteIndex = Integer.parseInt(streamPart.replace("index=", "")); - } else if (streamPart.contains("TAG:language=")) { - language = streamPart.replace("TAG:language=", ""); - } else if (streamPart.contains("channels=")) { - channels = Integer.parseInt(streamPart.replace("channels=", "")); - } else if (streamPart.contains("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

A list of parameters belonging to a subtitle stream.

- * @param relativeIndex

The relative index of the subtitle.

- * @return

A SubtitleStream object.

- * @throws NumberFormatException

If codec index contains a non-numeric value.

- */ - private static SubtitleStream parseSubtitleStream(String[] streamParts, int relativeIndex) - throws NumberFormatException { - String codecName = null; - int absoluteIndex = -1; - String language = null; - String title = ""; - for (String streamPart : streamParts) { - if (streamPart.contains("codec_name=")) { - codecName = streamPart.replace("codec_name=", ""); - } else if (streamPart.contains("index=")) { - absoluteIndex = Integer.parseInt(streamPart.replace("index=", "")); - } else if (streamPart.contains("TAG:language=")) { - language = streamPart.replace("TAG:language=", ""); - } else if (streamPart.contains("TAG:title=")) { - title = streamPart.replace("TAG:title=", ""); - } - } - return new SubtitleStream(codecName, absoluteIndex, relativeIndex, language, title); - } - - /** - * Escapes special characters which can cause trouble for ffmpeg - * @param fileName

The filename to escape.

- * @return

A filename with known special characters escaped.

- */ - static String escapeSpecialCharactersInFileName(String fileName) { - return fileName.replace("'", "\\\\\\'") - .replace(",", "\\\\\\,") - .replace(";", "\\;") - .replace("]", "\\]") - .replace("[", "\\["); - } - - /** - * Prints something to the commandline efficiently - * @param input

The text to print.

- * @throws IOException

If a write is not possible.

- */ - private static void print(String input) throws IOException { - if (!input.equals("")) { - writer.write(input); - writer.flush(); - } - } - - /** - * Prints something and a newline to the commandline efficiently - * @param input

The text to print.

- * @throws IOException

If a write is not possible.

- */ - static void printl(String input) throws IOException { - if (!input.equals("")) { - writer.write(input); - } - printl(); - } - - /** - * Prints a newline - * @throws IOException

If a write is not possible.

- */ - static void printl() throws IOException { - writer.newLine(); - writer.flush(); - } - - /** - * Checks whether there exists an external image subtitle with the same filename as the file - * @param directory

The directory containing the file.

- * @param file

The file to be converted.

- * @return

The extension of the subtitle or empty if no subtitle was found.

- */ - String hasExternalImageSubtitle(String directory, String file) { - String path = stripExtension(file); - for (String subtitleExtension : new String[] {".idx", ".sub"}) { - if (new File(directory + File.separator + path + subtitleExtension).exists()) { - return path + subtitleExtension; - } - } - return ""; - } - - /** - * Checks whether there exists an external subtitle with the same filename as the file - * @param directory

The directory containing the file.

- * @param file

The file to be converted.

- * @return

The extension of the subtitle or empty if no subtitle was found.

- */ - String hasExternalSubtitle(String directory, String file) { - String path = stripExtension(file); - for (String subtitleExtension : new String[] {".srt", ".ass"}) { - System.out.println(directory + File.separator + path + subtitleExtension); - if (new File(directory + File.separator + path + subtitleExtension).exists()) { - return path + subtitleExtension; - } - } - return ""; - } + void convert(File file) throws IOException; }