From 92b46bdc9e11bb1edb87face6e39f3e14bf11013 Mon Sep 17 00:00:00 2001 From: EpicKnarvik97 Date: Sun, 21 Apr 2024 19:54:45 +0200 Subject: [PATCH] Adds external audio parsing and other fixes 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 --- .../converter/AbstractConverter.java | 3 +- .../ffmpegconverter/property/StreamType.java | 28 ++++ .../streams/SubtitleStream.java | 2 +- .../ffmpegconverter/utility/FFMpegHelper.java | 121 +++++++++++++----- 4 files changed, 117 insertions(+), 37 deletions(-) create mode 100644 src/main/java/net/knarcraft/ffmpegconverter/property/StreamType.java diff --git a/src/main/java/net/knarcraft/ffmpegconverter/converter/AbstractConverter.java b/src/main/java/net/knarcraft/ffmpegconverter/converter/AbstractConverter.java index b987af0..b82118b 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/converter/AbstractConverter.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/converter/AbstractConverter.java @@ -48,7 +48,8 @@ public abstract class AbstractConverter implements Converter { @Override public void convert(@NotNull File file) throws IOException { - StreamProbeResult probeResult = FFMpegHelper.probeFile(this.ffprobePath, file, this.subtitleFormats); + StreamProbeResult probeResult = FFMpegHelper.probeFile(this.ffprobePath, file, this.subtitleFormats, + this.audioFormats); if (probeResult.parsedStreams().isEmpty()) { throw new IllegalArgumentException("The file has no valid streams. Please make sure the file exists and" + " is not corrupt."); diff --git a/src/main/java/net/knarcraft/ffmpegconverter/property/StreamType.java b/src/main/java/net/knarcraft/ffmpegconverter/property/StreamType.java new file mode 100644 index 0000000..23d5062 --- /dev/null +++ b/src/main/java/net/knarcraft/ffmpegconverter/property/StreamType.java @@ -0,0 +1,28 @@ +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, + + /** + * None of the above + */ + OTHER + +} diff --git a/src/main/java/net/knarcraft/ffmpegconverter/streams/SubtitleStream.java b/src/main/java/net/knarcraft/ffmpegconverter/streams/SubtitleStream.java index 7fa296e..97709f9 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/streams/SubtitleStream.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/streams/SubtitleStream.java @@ -50,7 +50,7 @@ public class SubtitleStream extends AbstractStream implements StreamObject { * @return

True if the subtitle translates everything.

*/ private boolean isFullSubtitle() { - String titleLowercase = getTitle().toLowerCase(); + String titleLowercase = getTitle().toLowerCase().trim(); return !titleLowercase.matches(".*si(ng|gn)s?[ &/a-z]+songs?.*") && !titleLowercase.matches(".*songs?[ &/a-z]+si(gn|ng)s?.*") && !titleLowercase.matches(".*forced.*") && diff --git a/src/main/java/net/knarcraft/ffmpegconverter/utility/FFMpegHelper.java b/src/main/java/net/knarcraft/ffmpegconverter/utility/FFMpegHelper.java index 1958052..8dd5e86 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/utility/FFMpegHelper.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/utility/FFMpegHelper.java @@ -3,6 +3,7 @@ package net.knarcraft.ffmpegconverter.utility; import net.knarcraft.ffmpegconverter.container.FFMpegCommand; import net.knarcraft.ffmpegconverter.container.ProcessResult; import net.knarcraft.ffmpegconverter.container.StreamProbeResult; +import net.knarcraft.ffmpegconverter.property.StreamType; import net.knarcraft.ffmpegconverter.streams.AudioStream; import net.knarcraft.ffmpegconverter.streams.OtherStream; import net.knarcraft.ffmpegconverter.streams.StreamObject; @@ -73,13 +74,15 @@ public final class FFMpegHelper { * @param ffprobePath

The path/command to ffprobe

* @param file

The file to probe

* @param subtitleFormats

The extensions to accept for external subtitles

+ * @param audioFormats

The extensions to accept for external audio files

* @return

A list of StreamObjects

* @throws IOException

If the process can't be readProcess

*/ @NotNull public static StreamProbeResult probeFile(@NotNull String ffprobePath, @NotNull File file, - @NotNull List subtitleFormats) throws IOException { - return parseStreams(ffprobePath, probeForStreams(ffprobePath, file), file, subtitleFormats); + @NotNull List subtitleFormats, + @NotNull List audioFormats) throws IOException { + return parseStreams(ffprobePath, probeForStreams(ffprobePath, file), file, subtitleFormats, audioFormats); } /** @@ -284,11 +287,27 @@ public final class FFMpegHelper { * @param streams

A list of all streams for the current file.

* @param file

The file currently being converted.

* @param subtitleFormats

The extensions to accept for external subtitles

+ * @param audioFormats

The extensions to accept for external audio tracks

* @return

A list of StreamObjects.

*/ @NotNull private static StreamProbeResult parseStreams(@NotNull String ffprobePath, @NotNull List streams, - @NotNull File file, @NotNull List subtitleFormats) throws IOException { + @NotNull File file, @NotNull List subtitleFormats, + @NotNull List audioFormats) throws IOException { + StreamProbeResult probeResult = new StreamProbeResult(new ArrayList<>(List.of(file)), parseStreamObjects(streams)); + getExternalStreams(probeResult, ffprobePath, file.getParentFile(), file.getName(), subtitleFormats); + getExternalStreams(probeResult, ffprobePath, file.getParentFile(), file.getName(), audioFormats); + return probeResult; + } + + /** + * Parses the stream objects found in the given streams + * + * @param streams

The stream data to parse

+ * @return

The parsed stream objects

+ */ + @NotNull + private static List parseStreamObjects(@NotNull List streams) { List parsedStreams = new ArrayList<>(); int relativeAudioIndex = 0; int relativeVideoIndex = 0; @@ -297,30 +316,50 @@ public final class FFMpegHelper { for (String stream : streams) { String[] streamParts = stream.split(PROBE_SPLIT_CHARACTER); Map streamInfo = getStreamInfo(streamParts); + StreamType streamType = getStreamType(streamInfo); - String codecType = ValueParsingHelper.parseString(streamInfo.get(StreamTag.CODEC_TYPE), ""); - switch (codecType) { - case "video": - // Some attached covers are marked as video streams - if (ValueParsingHelper.parseInt(streamInfo.get(StreamTag.DISPOSITION_ATTACHED_PIC), 0) != 1) { - parsedStreams.add(new VideoStream(streamInfo, 0, relativeVideoIndex++)); - } else { - parsedStreams.add(new OtherStream(streamInfo, 0)); - } + switch (streamType) { + case VIDEO: + parsedStreams.add(new VideoStream(streamInfo, 0, relativeVideoIndex++)); break; - case "audio": + case AUDIO: parsedStreams.add(new AudioStream(streamInfo, 0, relativeAudioIndex++)); break; - case "subtitle": + case SUBTITLE: parsedStreams.add(new SubtitleStream(streamInfo, 0, relativeSubtitleIndex++)); break; - default: + case OTHER: parsedStreams.add(new OtherStream(streamInfo, 0)); + break; } } - StreamProbeResult probeResult = new StreamProbeResult(List.of(file), parsedStreams); - getExternalSubtitles(probeResult, ffprobePath, file.getParentFile(), file.getName(), subtitleFormats); - return probeResult; + return parsedStreams; + } + + /** + * Gets the type of a stream from its stream info + * + * @param streamInfo

The information describing the stream

+ * @return

The type of the stream

+ */ + @NotNull + private static StreamType getStreamType(@NotNull Map streamInfo) { + String codecType = ValueParsingHelper.parseString(streamInfo.get(StreamTag.CODEC_TYPE), ""); + switch (codecType) { + case "video": + // Some attached covers are marked as video streams + if (ValueParsingHelper.parseInt(streamInfo.get(StreamTag.DISPOSITION_ATTACHED_PIC), 0) != 1) { + return StreamType.VIDEO; + } else { + return StreamType.OTHER; + } + case "audio": + return StreamType.AUDIO; + case "subtitle": + return StreamType.SUBTITLE; + default: + return StreamType.OTHER; + } } /** @@ -337,52 +376,64 @@ public final class FFMpegHelper { continue; } String[] keyValue = part.split("="); + String value = keyValue.length > 1 ? keyValue[1] : ""; + StreamTag tag = StreamTag.getFromString(keyValue[0]); if (tag != null) { - streamInfo.put(tag, keyValue[1]); + streamInfo.put(tag, value); } } return streamInfo; } /** - * Tries to find any external subtitles adjacent to the first input file, and appends it to the given probe result + * Tries to find any external files adjacent to the first input file, and appends it to the given probe result * * @param streamProbeResult

The stream probe result to append to

* @param ffprobePath

The path/command to ffprobe

* @param directory

The directory containing the file

* @param convertingFile

The first/main file to be converted

- * @param subtitleFormats

The extensions to accept for external subtitles

+ * @param formats

The extensions to accept for external tracks

*/ - private static void getExternalSubtitles(@NotNull StreamProbeResult streamProbeResult, - @NotNull String ffprobePath, @NotNull File directory, - @NotNull String convertingFile, @NotNull List subtitleFormats) throws IOException { + private static void getExternalStreams(@NotNull StreamProbeResult streamProbeResult, + @NotNull String ffprobePath, @NotNull File directory, + @NotNull String convertingFile, + @NotNull List formats) throws IOException { //Find all files in the same directory with external subtitle formats - File[] subtitleFiles = FileUtil.listFilesRecursive(directory, subtitleFormats, 1); - // TODO: Generalize this for external audio tracks + File[] files = FileUtil.listFilesRecursive(directory, formats, 1); //Return early if no files were found - if (subtitleFiles == null) { + if (files == null) { return; } String fileTitle = FileUtil.stripExtension(convertingFile); - List subtitleFilesList = new ArrayList<>(Arrays.asList(subtitleFiles)); + List filesList = new ArrayList<>(Arrays.asList(files)); //Finds the files which are subtitles probably belonging to the file - subtitleFilesList = ListUtil.getMatching(subtitleFilesList, - (subtitleFile) -> subtitleFile.getName().contains(fileTitle)); + filesList = ListUtil.getMatching(filesList, (file) -> file.getName().contains(fileTitle)); - for (File subtitleFile : subtitleFilesList) { + for (File file : filesList) { int inputIndex = streamProbeResult.parsedFiles().size(); - streamProbeResult.parsedFiles().add(subtitleFile); + streamProbeResult.parsedFiles().add(file); //Probe the files and add them to the result list - List streams = probeForStreams(ffprobePath, subtitleFile); - int relativeIndex = 0; + List streams = probeForStreams(ffprobePath, file); + + int audioIndex = 0; + int subtitleIndex = 0; + int videoIndex = 0; for (String stream : streams) { String[] streamParts = stream.split(PROBE_SPLIT_CHARACTER); Map streamInfo = getStreamInfo(streamParts); - streamProbeResult.parsedStreams().add(new SubtitleStream(streamInfo, inputIndex, relativeIndex++)); + 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); + } } } }