Compare commits

...

3 Commits

Author SHA1 Message Date
4fdbfb28e3 Adds an audio extractor
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
2024-10-12 01:37:40 +02:00
87f5743a24 Adds option for overwriting original files
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
2024-07-18 14:07:51 +02:00
972691db76 Removes inclusion of external audio files, as ffmpeg produces audio streams with no sound
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
2024-07-10 19:53:03 +02:00
9 changed files with 132 additions and 22 deletions

View File

@ -3,6 +3,7 @@ package net.knarcraft.ffmpegconverter;
import net.knarcraft.ffmpegconverter.config.Configuration; import net.knarcraft.ffmpegconverter.config.Configuration;
import net.knarcraft.ffmpegconverter.converter.AnimeConverter; import net.knarcraft.ffmpegconverter.converter.AnimeConverter;
import net.knarcraft.ffmpegconverter.converter.AudioConverter; import net.knarcraft.ffmpegconverter.converter.AudioConverter;
import net.knarcraft.ffmpegconverter.converter.AudioExtractor;
import net.knarcraft.ffmpegconverter.converter.AudioToVorbisConverter; import net.knarcraft.ffmpegconverter.converter.AudioToVorbisConverter;
import net.knarcraft.ffmpegconverter.converter.Converter; import net.knarcraft.ffmpegconverter.converter.Converter;
import net.knarcraft.ffmpegconverter.converter.DownScaleConverter; import net.knarcraft.ffmpegconverter.converter.DownScaleConverter;
@ -100,13 +101,14 @@ public class FFMpegConvert {
10. Anime to h265 all streams 10. Anime to h265 all streams
11. Stream reorder 11. Stream reorder
12. Letterbox cropper 12. Letterbox cropper
13. Video's Audio to vorbis converter""", 1, 13); 13. Video's Audio to vorbis converter
14. Audio from video extractor""", 1, 14, Integer.MIN_VALUE);
return switch (choice) { return switch (choice) {
case 1 -> generateWebAnimeConverter(); case 1 -> generateWebAnimeConverter();
case 2 -> new AudioConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output extension>")); case 2 -> new AudioConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output audio extension>", null));
case 3 -> new VideoConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output extension>")); case 3 -> new VideoConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output video extension>", null));
case 4 -> new WebVideoConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output extension>")); case 4 -> new WebVideoConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output video extension>", null));
case 5 -> new MkvH264Converter(FFPROBE_PATH, FFMPEG_PATH); case 5 -> new MkvH264Converter(FFPROBE_PATH, FFMPEG_PATH);
case 6 -> new MkvH265ReducedConverter(FFPROBE_PATH, FFMPEG_PATH); case 6 -> new MkvH265ReducedConverter(FFPROBE_PATH, FFMPEG_PATH);
case 7 -> generateMKVToMP4Transcoder(); case 7 -> generateMKVToMP4Transcoder();
@ -116,6 +118,8 @@ public class FFMpegConvert {
case 11 -> generateStreamOrderConverter(); case 11 -> generateStreamOrderConverter();
case 12 -> new LetterboxCropper(FFPROBE_PATH, FFMPEG_PATH); case 12 -> new LetterboxCropper(FFPROBE_PATH, FFMPEG_PATH);
case 13 -> new AudioToVorbisConverter(FFPROBE_PATH, FFMPEG_PATH); case 13 -> new AudioToVorbisConverter(FFPROBE_PATH, FFMPEG_PATH);
case 14 -> new AudioExtractor(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output audio extension>",
"mp3"), getChoice("<stream to extract>", 0, 1000, 0));
default -> null; default -> null;
}; };
} }
@ -182,9 +186,9 @@ public class FFMpegConvert {
private static Converter generateMKVToMP4Transcoder() { private static Converter generateMKVToMP4Transcoder() {
OutputUtil.println("[Audio stream index 0-n] [Subtitle stream index 0-n] [Video stream index 0-n]\nYour input: "); OutputUtil.println("[Audio stream index 0-n] [Subtitle stream index 0-n] [Video stream index 0-n]\nYour input: ");
List<String> input = readInput(3); List<String> input = readInput(3);
int audioStreamIndex = -1; int audioStreamIndex = 0;
int subtitleStreamIndex = -1; int subtitleStreamIndex = 0;
int videoStreamIndex = -1; int videoStreamIndex = 0;
try { try {
if (!input.isEmpty()) { if (!input.isEmpty()) {
@ -331,16 +335,20 @@ public class FFMpegConvert {
/** /**
* Gets the user's choice * Gets the user's choice
* *
* @param prompt <p>The prompt shown to the user.</p> * @param prompt <p>The prompt shown to the user.</p>
* @param defaultValue <p>The default value, if no input is given</p>
* @return <p>The non-empty choice given by the user.</p> * @return <p>The non-empty choice given by the user.</p>
*/ */
@NotNull @NotNull
private static String getChoice(@NotNull String prompt) { private static String getChoice(@NotNull String prompt, @Nullable Object defaultValue) {
OutputUtil.println(prompt); OutputUtil.println(prompt);
String choice = ""; String choice = "";
while (choice.isEmpty()) { while (choice.isEmpty()) {
OutputUtil.println("Your input: "); OutputUtil.println("Your input: ");
choice = READER.nextLine(); choice = READER.nextLine();
if (choice.isEmpty() && defaultValue != null) {
return String.valueOf(defaultValue);
}
} }
return choice; return choice;
} }
@ -353,7 +361,7 @@ public class FFMpegConvert {
* @param max The maximum allowed value * @param max The maximum allowed value
* @return The value given by the user * @return The value given by the user
*/ */
private static int getChoice(@NotNull String prompt, int min, int max) { private static int getChoice(@NotNull String prompt, int min, int max, int defaultValue) {
OutputUtil.println(prompt); OutputUtil.println(prompt);
int choice = Integer.MIN_VALUE; int choice = Integer.MIN_VALUE;
do { do {
@ -361,6 +369,9 @@ public class FFMpegConvert {
try { try {
choice = Integer.parseInt(READER.next()); choice = Integer.parseInt(READER.next());
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
if (defaultValue != Integer.MIN_VALUE) {
return defaultValue;
}
OutputUtil.println("Invalid choice. Please try again."); OutputUtil.println("Invalid choice. Please try again.");
} finally { } finally {
READER.nextLine(); READER.nextLine();

View File

@ -62,6 +62,11 @@ public class ConfigKey<T> {
*/ */
public static final ConfigKey<Boolean> COPY_ATTACHED_IMAGES = createKey("copy-attached-images", Boolean.class, false); public static final ConfigKey<Boolean> COPY_ATTACHED_IMAGES = createKey("copy-attached-images", Boolean.class, false);
/**
* The configuration key for whether to overwrite the original file when converting
*/
public static final ConfigKey<Boolean> OVERWRITE_FILES = createKey("overwrite-files", Boolean.class, false);
private final String configKey; private final String configKey;
private final T defaultValue; private final T defaultValue;
private final Class<T> type; private final Class<T> type;

View File

@ -23,6 +23,7 @@ public class Configuration {
private MinimalSubtitlePreference minimalSubtitlePreference; private MinimalSubtitlePreference minimalSubtitlePreference;
private boolean deInterlaceVideo; private boolean deInterlaceVideo;
private boolean copyAttachedImages; private boolean copyAttachedImages;
private boolean overwriteFiles;
/** /**
* Instantiates and loads a new configuration * Instantiates and loads a new configuration
@ -57,6 +58,7 @@ public class Configuration {
animeSubtitleLanguages = ConfigHelper.asStringList(configHandler.getValue(ConfigKey.SUBTITLE_LANGUAGES_ANIME)); animeSubtitleLanguages = ConfigHelper.asStringList(configHandler.getValue(ConfigKey.SUBTITLE_LANGUAGES_ANIME));
deInterlaceVideo = ConfigHelper.asBoolean(configHandler.getValue(ConfigKey.DE_INTERLACE_VIDEO)); deInterlaceVideo = ConfigHelper.asBoolean(configHandler.getValue(ConfigKey.DE_INTERLACE_VIDEO));
copyAttachedImages = ConfigHelper.asBoolean(configHandler.getValue(ConfigKey.COPY_ATTACHED_IMAGES)); copyAttachedImages = ConfigHelper.asBoolean(configHandler.getValue(ConfigKey.COPY_ATTACHED_IMAGES));
overwriteFiles = ConfigHelper.asBoolean(configHandler.getValue(ConfigKey.OVERWRITE_FILES));
try { try {
minimalSubtitlePreference = MinimalSubtitlePreference.valueOf(String.valueOf(configHandler.getValue( minimalSubtitlePreference = MinimalSubtitlePreference.valueOf(String.valueOf(configHandler.getValue(
ConfigKey.MINIMAL_SUBTITLE_PREFERENCE))); ConfigKey.MINIMAL_SUBTITLE_PREFERENCE)));
@ -171,4 +173,13 @@ public class Configuration {
return this.copyAttachedImages; return this.copyAttachedImages;
} }
/**
* Gets whether the original files should be overwritten after conversion has been finished
*
* @return <p>True if the original file should be overwritten</p>
*/
public boolean overwriteFiles() {
return this.overwriteFiles;
}
} }

View File

@ -51,8 +51,7 @@ public abstract class AbstractConverter implements Converter {
@Override @Override
public void convert(@NotNull File file) throws IOException { 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()) { if (probeResult.parsedStreams().isEmpty()) {
throw new IllegalArgumentException("The file has no valid streams. Please make sure the file exists and" + throw new IllegalArgumentException("The file has no valid streams. Please make sure the file exists and" +
" is not corrupt."); " is not corrupt.");
@ -86,6 +85,16 @@ public abstract class AbstractConverter implements Converter {
int exitCode = FFMpegHelper.runProcess(processBuilder, file.getParentFile(), "\n", true).exitCode(); int exitCode = FFMpegHelper.runProcess(processBuilder, file.getParentFile(), "\n", true).exitCode();
if (exitCode != 0) { if (exitCode != 0) {
handleError(ffMpegCommand, file, newPath); handleError(ffMpegCommand, file, newPath);
} else if (FFMpegConvert.getConfiguration().overwriteFiles() &&
FileUtil.getExtension(newPath).equalsIgnoreCase(FileUtil.getExtension(file.getPath()))) {
File outputFile = new File(newPath);
if (!file.delete()) {
OutputUtil.println("Unable to remove original file.");
System.exit(1);
} else if (!outputFile.renameTo(file)) {
OutputUtil.println("Failed to re-name file after conversion!");
System.exit(1);
}
} }
} }

View File

@ -0,0 +1,65 @@
package net.knarcraft.ffmpegconverter.converter;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
import net.knarcraft.ffmpegconverter.converter.module.DebugModule;
import net.knarcraft.ffmpegconverter.converter.module.ModuleExecutor;
import net.knarcraft.ffmpegconverter.converter.module.mapping.NthAudioStreamModule;
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* An extractor for getting audio streams from video files
*/
public class AudioExtractor extends AbstractConverter {
private final int streamToExtract;
/**
* Instantiates a new audio extractor
*
* @param ffprobePath <p>Path/command to ffprobe.</p>
* @param ffmpegPath <p>Path/command to ffmpeg.</p>
* @param newExtension <p>The extension of the new file.</p>
* @param streamToExtract <p>The stream to be extracted from the video file</p>
*/
public AudioExtractor(@NotNull String ffprobePath, @NotNull String ffmpegPath, @NotNull String newExtension,
int streamToExtract) {
super(newExtension);
this.ffprobePath = ffprobePath;
this.ffmpegPath = ffmpegPath;
this.streamToExtract = Math.max(0, streamToExtract);
}
@Override
@Nullable
public FFMpegCommand generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
@NotNull String outFile) {
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
List<ConverterModule> modules = new ArrayList<>();
if (this.debug) {
modules.add(new DebugModule());
}
//Gets the first audio stream from the file and adds it to the output file
modules.add(new NthAudioStreamModule(probeResult.getAudioStreams(), streamToExtract));
modules.add(new SetOutputFileModule(outFile));
new ModuleExecutor(command, modules).execute();
return command;
}
@Override
@NotNull
public List<String> getValidFormats() {
return videoFormats;
}
}

View File

@ -27,6 +27,13 @@ public enum StreamType {
*/ */
COVER_IMAGE, COVER_IMAGE,
/**
* Binary data
*
* <p>Binary data streams only cause problems, as they cannot, for example, be included in an MKV file.</p>
*/
DATA,
/** /**
* None of the above * None of the above
*/ */

View File

@ -41,15 +41,13 @@ public final class FFMpegHelper {
* @param ffprobePath <p>The path/command to ffprobe</p> * @param ffprobePath <p>The path/command to ffprobe</p>
* @param file <p>The file to probe</p> * @param file <p>The file to probe</p>
* @param subtitleFormats <p>The extensions to accept for external subtitles</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> * @return <p>A list of StreamObjects</p>
* @throws IOException <p>If the process can't be readProcess</p> * @throws IOException <p>If the process can't be readProcess</p>
*/ */
@NotNull @NotNull
public static StreamProbeResult probeFile(@NotNull String ffprobePath, @NotNull File file, public static StreamProbeResult probeFile(@NotNull String ffprobePath, @NotNull File file,
@NotNull List<String> subtitleFormats, @NotNull List<String> subtitleFormats) throws IOException {
@NotNull List<String> audioFormats) throws IOException { return parseStreams(ffprobePath, probeForStreams(ffprobePath, file), file, subtitleFormats);
return parseStreams(ffprobePath, probeForStreams(ffprobePath, file), file, subtitleFormats, audioFormats);
} }
/** /**
@ -277,17 +275,14 @@ public final class FFMpegHelper {
* @param streams <p>A list of all streams for the current file.</p> * @param streams <p>A list of all streams for the current file.</p>
* @param file <p>The file currently being converted.</p> * @param file <p>The file currently being converted.</p>
* @param subtitleFormats <p>The extensions to accept for external subtitles</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> * @return <p>A list of StreamObjects.</p>
*/ */
@NotNull @NotNull
private static StreamProbeResult parseStreams(@NotNull String ffprobePath, @NotNull List<String> streams, private static StreamProbeResult parseStreams(@NotNull String ffprobePath, @NotNull List<String> streams,
@NotNull File file, @NotNull List<String> subtitleFormats, @NotNull File file, @NotNull List<String> subtitleFormats) throws IOException {
@NotNull List<String> audioFormats) throws IOException {
StreamProbeResult probeResult = new StreamProbeResult(new ArrayList<>(List.of(file)), StreamProbeResult probeResult = new StreamProbeResult(new ArrayList<>(List.of(file)),
parseStreamObjects(streams)); parseStreamObjects(streams));
getExternalStreams(probeResult, ffprobePath, file.getParentFile(), file.getName(), subtitleFormats); getExternalStreams(probeResult, ffprobePath, file.getParentFile(), file.getName(), subtitleFormats);
getExternalStreams(probeResult, ffprobePath, file.getParentFile(), file.getName(), audioFormats);
return probeResult; return probeResult;
} }
@ -319,6 +314,8 @@ public final class FFMpegHelper {
parsedStreams.add(new OtherStream(streamInfo, 0, true)); parsedStreams.add(new OtherStream(streamInfo, 0, true));
} }
} }
case DATA -> OutputUtil.print("A binary stream was found. Those are ignored they will generally " +
"cause the conversion to fail.");
} }
} }
return parsedStreams; return parsedStreams;
@ -347,6 +344,8 @@ public final class FFMpegHelper {
return StreamType.AUDIO; return StreamType.AUDIO;
case "subtitle": case "subtitle":
return StreamType.SUBTITLE; return StreamType.SUBTITLE;
case "data":
return StreamType.DATA;
default: default:
return StreamType.OTHER; return StreamType.OTHER;
} }

View File

@ -37,4 +37,5 @@ wma
wv wv
webm webm
8svx 8svx
mka mka
ac3

View File

@ -19,4 +19,6 @@ minimal-subtitle-preference=AVOID
# The preference for whether video streams should be de-interlaced. It is recommended to only enable this when you notice that a video file is interlaced. # The preference for whether video streams should be de-interlaced. It is recommended to only enable this when you notice that a video file is interlaced.
de-interlace-video=false de-interlace-video=false
# Whether to copy attached cover images. FFMpeg sometimes throws errors when including attached images. # Whether to copy attached cover images. FFMpeg sometimes throws errors when including attached images.
copy-attached-images=false copy-attached-images=false
# Whether to overwrite original files after conversion. Note that if enabled, the original files are lost, which is troublesome if the conversion arguments are incorrect.
overwrite-files=false