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
This commit is contained in:
Kristian Knarvik 2024-04-21 19:54:45 +02:00
parent ded88eb5b5
commit 92b46bdc9e
4 changed files with 117 additions and 37 deletions

View File

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

View File

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

View File

@ -50,7 +50,7 @@ public class SubtitleStream extends AbstractStream implements StreamObject {
* @return <p>True if the subtitle translates everything.</p>
*/
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.*") &&

View File

@ -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 <p>The path/command to ffprobe</p>
* @param file <p>The file to probe</p>
* @param subtitleFormats <p>The extensions to accept for external subtitles</p>
* @param audioFormats <p>The extensions to accept for external audio files</p>
* @return <p>A list of StreamObjects</p>
* @throws IOException <p>If the process can't be readProcess</p>
*/
@NotNull
public static StreamProbeResult probeFile(@NotNull String ffprobePath, @NotNull File file,
@NotNull List<String> subtitleFormats) throws IOException {
return parseStreams(ffprobePath, probeForStreams(ffprobePath, file), file, subtitleFormats);
@NotNull List<String> subtitleFormats,
@NotNull List<String> audioFormats) throws IOException {
return parseStreams(ffprobePath, probeForStreams(ffprobePath, file), file, subtitleFormats, audioFormats);
}
/**
@ -284,11 +287,27 @@ public final class FFMpegHelper {
* @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>
* @param audioFormats <p>The extensions to accept for external audio tracks</p>
* @return <p>A list of StreamObjects.</p>
*/
@NotNull
private static StreamProbeResult parseStreams(@NotNull String ffprobePath, @NotNull List<String> streams,
@NotNull File file, @NotNull List<String> subtitleFormats) throws IOException {
@NotNull File file, @NotNull List<String> subtitleFormats,
@NotNull List<String> 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 <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<>();
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<StreamTag, String> 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 <p>The information describing the stream</p>
* @return <p>The type of the stream</p>
*/
@NotNull
private static StreamType getStreamType(@NotNull Map<StreamTag, String> 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 <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 subtitleFormats <p>The extensions to accept for external subtitles</p>
* @param formats <p>The extensions to accept for external tracks</p>
*/
private static void getExternalSubtitles(@NotNull StreamProbeResult streamProbeResult,
@NotNull String ffprobePath, @NotNull File directory,
@NotNull String convertingFile, @NotNull List<String> subtitleFormats) throws IOException {
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
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<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
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<String> streams = probeForStreams(ffprobePath, subtitleFile);
int relativeIndex = 0;
List<String> 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<StreamTag, String> 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);
}
}
}
}