Improves configuration handling and stuff
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good

This commit is contained in:
Kristian Knarvik 2024-05-19 10:16:51 +02:00
parent ac25ca1986
commit 380a1b800a
22 changed files with 598 additions and 148 deletions

View File

@ -1,7 +1,6 @@
package net.knarcraft.ffmpegconverter;
import net.knarcraft.ffmpegconverter.config.ConfigHandler;
import net.knarcraft.ffmpegconverter.config.ConfigKey;
import net.knarcraft.ffmpegconverter.config.Configuration;
import net.knarcraft.ffmpegconverter.converter.AnimeConverter;
import net.knarcraft.ffmpegconverter.converter.AudioConverter;
import net.knarcraft.ffmpegconverter.converter.Converter;
@ -15,9 +14,7 @@ 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.ListUtil;
import net.knarcraft.ffmpegconverter.utility.OutputUtil;
import org.apache.commons.configuration2.Configuration;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -38,19 +35,10 @@ public class FFMpegConvert {
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 ConfigHandler configHandler = new ConfigHandler();
private static boolean debug = false;
private static boolean useHardwareAcceleration = false;
private static final Configuration configuration = new Configuration();
public static void main(@NotNull String[] arguments) throws IOException {
Configuration configuration = configHandler.load();
if (configuration.containsKey(ConfigKey.DEBUG.toString())) {
debug = configuration.getBoolean(ConfigKey.DEBUG.toString());
}
OutputUtil.setDebug(debug);
if (configuration.containsKey(ConfigKey.USE_HARDWARE_ACCELERATION.toString())) {
useHardwareAcceleration = configuration.getBoolean(ConfigKey.USE_HARDWARE_ACCELERATION.toString());
}
OutputUtil.setDebug(configuration.isDebugEnabled());
converter = loadConverter();
@ -85,27 +73,10 @@ public class FFMpegConvert {
* @return <p>The configuration handler</p>
*/
@NotNull
public static ConfigHandler getConfigHandler() {
return configHandler;
public static Configuration getConfiguration() {
return configuration;
}
/**
* Gets whether debug mode is enabled
*
* @return <p>True if debug mode is enabled</p>
*/
public static boolean isDebugEnabled() {
return debug;
}
/**
* Gets whether hardware accelerated encoding is enabled
*
* @return <p>True if hardware accelerated encoding is enabled</p>
*/
public static boolean useHardwareAcceleration() {
return useHardwareAcceleration;
}
/**
* Asks the user which converter they want, and assigns a converter instance to the converter variable
@ -230,51 +201,37 @@ public class FFMpegConvert {
*/
@Nullable
private static Converter generateAnimeConverter() {
OutputUtil.println("[Audio languages jpn,eng,ger,fre] [Subtitle languages eng,ger,fre] [Minimal subtitle " +
"preference REQUIRE/PREFER/NO_PREFERENCE/AVOID/REJECT] [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(8);
String[] audioLanguage = new String[]{"jpn", "nor", "eng", "0"};
String[] subtitleLanguage = new String[]{"nob", "nor", "eng", "jpn", "0"};
MinimalSubtitlePreference subtitlePreference = MinimalSubtitlePreference.AVOID;
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;
if (!input.isEmpty()) {
audioLanguage = ListUtil.getListFromCommaSeparatedString(input.get(0));
}
if (input.size() > 1) {
subtitleLanguage = ListUtil.getListFromCommaSeparatedString(input.get(1));
}
if (input.size() > 2) {
subtitlePreference = MinimalSubtitlePreference.valueOf(input.get(2).toUpperCase());
}
try {
if (input.size() > 3) {
forcedAudioIndex = Integer.parseInt(input.get(3));
if (!input.isEmpty()) {
forcedAudioIndex = Integer.parseInt(input.get(0));
}
if (input.size() > 4) {
forcedSubtitleIndex = Integer.parseInt(input.get(4));
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() > 5) {
forceVideoEncoding = Boolean.parseBoolean(input.get(5));
if (input.size() > 2) {
forceVideoEncoding = Boolean.parseBoolean(input.get(2));
}
if (input.size() > 6) {
forceAudioEncoding = Boolean.parseBoolean(input.get(6));
if (input.size() > 3) {
forceAudioEncoding = Boolean.parseBoolean(input.get(3));
}
if (input.size() > 7) {
subtitleNameFilter = input.get(7);
if (input.size() > 4) {
subtitleNameFilter = input.get(4);
}
return new AnimeConverter(FFPROBE_PATH, FFMPEG_PATH, audioLanguage, subtitleLanguage, subtitlePreference,
forcedAudioIndex, forcedSubtitleIndex, subtitleNameFilter, forceVideoEncoding, forceAudioEncoding);
return new AnimeConverter(FFPROBE_PATH, FFMPEG_PATH, forcedAudioIndex, forcedSubtitleIndex, subtitleNameFilter,
forceVideoEncoding, forceAudioEncoding);
}
/**
@ -284,12 +241,9 @@ public class FFMpegConvert {
*/
@Nullable
private static Converter generateWebAnimeConverter() {
OutputUtil.println("[Audio languages jpn,eng,ger,fre] [Subtitle languages eng,ger,fre] [Convert to stereo if " +
"necessary true/false] [Prevent signs&songs subtitles true/false] [Forced audio index 0-n] " +
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(7);
String[] audioLanguage = new String[]{"jpn", "0"};
String[] subtitleLanguage = new String[]{"eng", "0"};
List<String> input = readInput(5);
boolean toStereo = true;
MinimalSubtitlePreference subtitlePreference = MinimalSubtitlePreference.AVOID;
int forcedAudioIndex = 0;
@ -297,32 +251,26 @@ public class FFMpegConvert {
String subtitleNameFilter = "";
if (!input.isEmpty()) {
audioLanguage = ListUtil.getListFromCommaSeparatedString(input.get(0));
toStereo = Boolean.parseBoolean(input.get(0));
}
if (input.size() > 1) {
subtitleLanguage = ListUtil.getListFromCommaSeparatedString(input.get(1));
}
if (input.size() > 2) {
toStereo = Boolean.parseBoolean(input.get(2));
}
if (input.size() > 3) {
subtitlePreference = MinimalSubtitlePreference.valueOf(input.get(3).toUpperCase());
subtitlePreference = MinimalSubtitlePreference.valueOf(input.get(1).toUpperCase());
}
try {
if (input.size() > 4) {
forcedAudioIndex = Integer.parseInt(input.get(4));
if (input.size() > 2) {
forcedAudioIndex = Integer.parseInt(input.get(2));
}
if (input.size() > 5) {
forcedSubtitleIndex = Integer.parseInt(input.get(5));
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() > 6) {
subtitleNameFilter = input.get(6);
if (input.size() > 4) {
subtitleNameFilter = input.get(4);
}
return new WebAnimeConverter(FFPROBE_PATH, FFMPEG_PATH, audioLanguage, subtitleLanguage, toStereo,
return new WebAnimeConverter(FFPROBE_PATH, FFMPEG_PATH, toStereo,
subtitlePreference, forcedAudioIndex, forcedSubtitleIndex, subtitleNameFilter);
}

View File

@ -8,10 +8,12 @@ import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder;
import org.apache.commons.configuration2.builder.fluent.Configurations;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
@ -23,6 +25,18 @@ public class ConfigHandler {
private final Configurations configurations = new Configurations();
private final File settingsFile = new File(configFolder, "config.properties");
private FileBasedConfigurationBuilder<PropertiesConfiguration> builder = configurations.propertiesBuilder(settingsFile);
private final Map<ConfigKey<?>, Object> optionValues = new HashMap<>();
/**
* Gets the current value of a configuration option
*
* @param key <p>The configuration key to get the value of</p>
* @return <p>The current value</p>
*/
@Nullable
public Object getValue(@NotNull ConfigKey<?> key) {
return optionValues.get(key);
}
/**
* Gets a writable configuration used for changing settings
@ -64,11 +78,9 @@ public class ConfigHandler {
/**
* Loads the saved configuration file
*
* @return <p>The loaded configuration</p>
*/
@NotNull
public Configuration load() throws IOException {
public void load() throws IOException {
optionValues.clear();
Configuration configuration;
if (!settingsFile.exists()) {
configuration = new PropertiesConfiguration();
@ -86,7 +98,14 @@ public class ConfigHandler {
}
// Reload contents in the builder
builder = configurations.propertiesBuilder(settingsFile);
return configuration;
for (@NotNull ConfigKey<?> key : ConfigKey.values()) {
if (configuration.containsKey(key.toString())) {
optionValues.put(key, configuration.get(key.getType(), key.toString()));
} else {
optionValues.put(key, key.getDefaultValue());
}
}
}
}

View File

@ -2,31 +2,106 @@ package net.knarcraft.ffmpegconverter.config;
import org.jetbrains.annotations.NotNull;
import java.util.HashSet;
import java.util.Set;
/**
* A representation of all configuration keys
*/
public enum ConfigKey {
public class ConfigKey<T> {
private static final Set<ConfigKey<?>> allKeys = new HashSet<>();
/**
* The configuration key for the list of hardware-accelerated encoders available on the system
*/
HARDWARE_ACCELERATED_ENCODERS("encoders-hardware-accelerated"),
public static final ConfigKey<String> HARDWARE_ACCELERATED_ENCODERS = new ConfigKey<>("encoders-hardware-accelerated", String.class, "qsv,cuda,dxva2,d3d11va,opencl,vulkan");
/**
* The configuration key for toggling debug mode
*/
DEBUG("debug"),
public static final ConfigKey<Boolean> DEBUG = createKey("debug", Boolean.class, false);
/**
* The configuration key for toggling hardware acceleration
* The configuration key for toggling hardware encoding
*/
USE_HARDWARE_ACCELERATION("hardware-acceleration"),
;
public static final ConfigKey<Boolean> USE_HARDWARE_ENCODING = createKey("hardware-acceleration-encode", Boolean.class, false);
/**
* The configuration key for toggling hardware decoding
*/
public static final ConfigKey<Boolean> USE_HARDWARE_DECODING = createKey("hardware-acceleration-decode", Boolean.class, true);
/**
* The configuration key for toggling forced flac encoding
*/
public static final ConfigKey<Boolean> ENCODE_FLAC_ALWAYS = createKey("encode-flac-always", Boolean.class, false);
/**
* The configuration key for anime audio languages
*/
public static final ConfigKey<String> AUDIO_LANGUAGES_ANIME = createKey("audio-languages-anime", String.class, "jpn,eng,*");
/**
* The configuration key for anime subtitle languages
*/
public static final ConfigKey<String> SUBTITLE_LANGUAGES_ANIME = createKey("subtitle-languages-anime", String.class, "eng,*");
/**
* The configuration key for the minimal subtitle preference
*/
public static final ConfigKey<String> MINIMAL_SUBTITLE_PREFERENCE = createKey("minimal-subtitle-preference", String.class, "AVOID");
private final String configKey;
private final T defaultValue;
private final Class<T> type;
ConfigKey(@NotNull String configKey) {
/**
* Instantiates a new config key
*
* @param configKey <p>The storage key in the configuration file</p>
* @param type <p>The type of this option's value</p>
* @param defaultValue <p>The default value of this option</p>
*/
private ConfigKey(@NotNull String configKey, @NotNull Class<T> type, @NotNull T defaultValue) {
this.configKey = configKey;
this.type = type;
this.defaultValue = defaultValue;
}
/**
* Creates a new config key
*
* @param configKey <p>The storage key in the configuration file</p>
* @param type <p>The type of this option's value</p>
* @param defaultValue <p>The default value of this option</p>
* @param <F> <p>The type of key to create</p>
* @return <p>The new config key</p>
*/
private static <F> ConfigKey<F> createKey(@NotNull String configKey, @NotNull Class<F> type, @NotNull F defaultValue) {
ConfigKey<F> key = new ConfigKey<>(configKey, type, defaultValue);
allKeys.add(key);
return key;
}
/**
* Gets the type of this option's value
*
* @return <p>The type of the value</p>
*/
@NotNull
public Class<T> getType() {
return this.type;
}
/**
* Gets the default value for the option represented by this key
*
* @return <p>The default value</p>
*/
@NotNull
public T getDefaultValue() {
return this.defaultValue;
}
@Override
@ -34,4 +109,14 @@ public enum ConfigKey {
return this.configKey;
}
/**
* Gets all configuration keys
*
* @return <p>All configuration keys</p>
*/
@NotNull
public static ConfigKey<?>[] values() {
return allKeys.toArray(new ConfigKey[0]);
}
}

View File

@ -0,0 +1,150 @@
package net.knarcraft.ffmpegconverter.config;
import net.knarcraft.ffmpegconverter.property.MinimalSubtitlePreference;
import net.knarcraft.ffmpegconverter.utility.ConfigHelper;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.util.List;
/**
* A configuration used for FFMpegConvert
*/
public class Configuration {
private final ConfigHandler configHandler;
private boolean debug;
private boolean useHardwareEncoding;
private boolean useHardwareDecoding;
private List<String> hardwareEncoders;
private boolean alwaysEncodeFlac;
private List<String> animeAudioLanguages;
private List<String> animeSubtitleLanguages;
private MinimalSubtitlePreference minimalSubtitlePreference;
/**
* Instantiates and loads a new configuration
*/
public Configuration() {
this.configHandler = new ConfigHandler();
try {
this.configHandler.load();
} catch (IOException e) {
throw new RuntimeException(e);
}
load();
}
/**
* Loads/reloads this configuration
*/
public void load() {
try {
this.configHandler.load();
} catch (IOException e) {
throw new RuntimeException(e);
}
debug = ConfigHelper.asBoolean(configHandler.getValue(ConfigKey.DEBUG));
useHardwareEncoding = ConfigHelper.asBoolean(configHandler.getValue(ConfigKey.USE_HARDWARE_ENCODING));
useHardwareDecoding = ConfigHelper.asBoolean(configHandler.getValue(ConfigKey.USE_HARDWARE_DECODING));
hardwareEncoders = ConfigHelper.asStringList(configHandler.getValue(ConfigKey.HARDWARE_ACCELERATED_ENCODERS));
alwaysEncodeFlac = ConfigHelper.asBoolean(configHandler.getValue(ConfigKey.ENCODE_FLAC_ALWAYS));
animeAudioLanguages = ConfigHelper.asStringList(configHandler.getValue(ConfigKey.AUDIO_LANGUAGES_ANIME));
animeSubtitleLanguages = ConfigHelper.asStringList(configHandler.getValue(ConfigKey.SUBTITLE_LANGUAGES_ANIME));
try {
minimalSubtitlePreference = MinimalSubtitlePreference.valueOf(String.valueOf(configHandler.getValue(
ConfigKey.MINIMAL_SUBTITLE_PREFERENCE)));
} catch (IllegalArgumentException | NullPointerException exception) {
minimalSubtitlePreference = MinimalSubtitlePreference.AVOID;
}
}
/**
* Gets the underlying config handler for this configuration
*
* @return <p>The underlying config handler</p>
*/
@NotNull
public ConfigHandler getConfigHandler() {
return this.configHandler;
}
/**
* Gets whether debug mode is enabled
*
* @return <p>True if debug mode is enabled</p>
*/
public boolean isDebugEnabled() {
return this.debug;
}
/**
* Gets whether hardware accelerated encoding is enabled
*
* @return <p>True if hardware accelerated encoding is enabled</p>
*/
public boolean useHardwareEncoding() {
return this.useHardwareEncoding;
}
/**
* Gets whether hardware accelerated decoding is enabled
*
* @return <p>True if hardware accelerated decoding is enabled</p>
*/
public boolean useHardwareDecoding() {
return this.useHardwareDecoding;
}
/**
* Gets whether FLAC audio tracks should always be re-encoded
*
* @return <p>Whether FLAC tracks should always be re-encoded</p>
*/
public boolean alwaysEncodeFlac() {
return this.alwaysEncodeFlac;
}
/**
* Gets all hardware encoders defined in the configuration
*
* @return <p>The specified hardware encoders</p>
*/
@NotNull
public List<String> hardwareEncoders() {
return this.hardwareEncoders;
}
/**
* Gets the audio language priorities for usage when converting anime
*
* @return <p>The anime audio language priorities</p>
*/
@NotNull
public List<String> getAnimeAudioLanguages() {
return this.animeAudioLanguages;
}
/**
* Gets the subtitle language priorities for usage when converting anime
*
* @return <p>The anime subtitle language priorities</p>
*/
@NotNull
public List<String> getAnimeSubtitleLanguages() {
return this.animeSubtitleLanguages;
}
/**
* Gets the preference for minimal subtitles
*
* @return <p>The minimal subtitle preference</p>
*/
@NotNull
public MinimalSubtitlePreference getMinimalSubtitlePreference() {
return this.minimalSubtitlePreference;
}
}

View File

@ -4,6 +4,7 @@ import net.knarcraft.ffmpegconverter.FFMpegConvert;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
import net.knarcraft.ffmpegconverter.handler.AvailableHardwareEncoderHandler;
import net.knarcraft.ffmpegconverter.streams.StreamObject;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
import net.knarcraft.ffmpegconverter.utility.FileHelper;
import net.knarcraft.ffmpegconverter.utility.FileUtil;
@ -21,7 +22,7 @@ import java.util.List;
*/
public abstract class AbstractConverter implements Converter {
final boolean debug = FFMpegConvert.isDebugEnabled();
final boolean debug = FFMpegConvert.getConfiguration().isDebugEnabled();
private final String newExtension;
protected String ffprobePath;
protected String ffmpegPath;
@ -136,4 +137,18 @@ public abstract class AbstractConverter implements Converter {
return encoderHandler.availableHardwareEncodings();
}
/**
* Sets the output indexes for the given streams
*
* <p>This will basically mark the given streams' order as the order they will appear in the output file. This is
* used when specifying specific codecs per-stream in the output file.</p>
*
* @param streams <p>The streams to set the output indexes for</p>
*/
protected <K extends StreamObject> void setOutputIndexes(@NotNull List<K> streams) {
for (int i = 0; i < streams.size(); i++) {
streams.get(i).setOutputIndex(i);
}
}
}

View File

@ -1,6 +1,7 @@
package net.knarcraft.ffmpegconverter.converter;
import net.knarcraft.ffmpegconverter.FFMpegConvert;
import net.knarcraft.ffmpegconverter.config.Configuration;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
@ -16,6 +17,7 @@ import net.knarcraft.ffmpegconverter.converter.module.output.CopyVideoModule;
import net.knarcraft.ffmpegconverter.converter.module.output.FastStartModule;
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
import net.knarcraft.ffmpegconverter.converter.module.output.SetQualityModule;
import net.knarcraft.ffmpegconverter.converter.module.output.SetStreamLanguageModule;
import net.knarcraft.ffmpegconverter.converter.module.output.SetVideoCodecModule;
import net.knarcraft.ffmpegconverter.converter.sorter.AudioLanguageSorter;
import net.knarcraft.ffmpegconverter.converter.sorter.ForcedFirstSorter;
@ -29,10 +31,12 @@ import net.knarcraft.ffmpegconverter.streams.AudioStream;
import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
import net.knarcraft.ffmpegconverter.streams.VideoStream;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
import net.knarcraft.ffmpegconverter.utility.OutputUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
@ -40,8 +44,8 @@ import java.util.List;
*/
public class AnimeConverter extends AbstractConverter {
private final String[] audioLanguages;
private final String[] subtitleLanguages;
private final List<String> audioLanguages;
private final List<String> subtitleLanguages;
private final MinimalSubtitlePreference subtitlePreference;
private final int forcedAudioIndex;
private final int forcedSubtitleIndex;
@ -54,25 +58,21 @@ public class AnimeConverter extends AbstractConverter {
*
* @param ffprobePath <p>Path/command to ffprobe.</p>
* @param ffmpegPath <p>Path/command to ffmpeg.</p>
* @param audioLanguages <p>List of wanted audio languages in descending order.</p>
* @param subtitleLanguages <p>List of wanted subtitle languages in descending order.</p>
* @param subtitlePreference <p>How minimal subtitles should be prioritized</p>
* @param forcedAudioIndex <p>A specific audio stream to force as default. 0-indexed from the first audio stream found</p>
* @param forcedSubtitleIndex <p>A specific subtitle stream to force as default. 0-indexed for the first subtitle stream found</p>
* @param forceVideoEncoding <p>Whether to enforce encoding on the video, even if already hevc</p>
* @param forceAudioEncoding <p>Whether to convert audio to web-playable, even though there should be no need</p>
*/
public AnimeConverter(@NotNull String ffprobePath, @NotNull String ffmpegPath, @NotNull String[] audioLanguages,
@NotNull String[] subtitleLanguages,
@NotNull MinimalSubtitlePreference subtitlePreference, int forcedAudioIndex,
public AnimeConverter(@NotNull String ffprobePath, @NotNull String ffmpegPath, int forcedAudioIndex,
int forcedSubtitleIndex, @NotNull String subtitleNameFilter, boolean forceVideoEncoding,
boolean forceAudioEncoding) {
super("mkv");
Configuration configuration = FFMpegConvert.getConfiguration();
this.ffprobePath = ffprobePath;
this.ffmpegPath = ffmpegPath;
this.audioLanguages = audioLanguages;
this.subtitleLanguages = subtitleLanguages;
this.subtitlePreference = subtitlePreference;
this.audioLanguages = configuration.getAnimeAudioLanguages();
this.subtitleLanguages = configuration.getAnimeSubtitleLanguages();
this.subtitlePreference = configuration.getMinimalSubtitlePreference();
this.forcedAudioIndex = forcedAudioIndex;
this.forcedSubtitleIndex = forcedSubtitleIndex;
this.subtitleNameFilter = subtitleNameFilter;
@ -85,6 +85,7 @@ public class AnimeConverter extends AbstractConverter {
public FFMpegCommand generateConversionCommand(@NotNull String executable,
@NotNull StreamProbeResult probeResult,
@NotNull String outFile) {
Configuration configuration = FFMpegConvert.getConfiguration();
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
List<ConverterModule> modules = new ArrayList<>();
if (this.debug) {
@ -92,16 +93,20 @@ public class AnimeConverter extends AbstractConverter {
}
modules.add(new FastStartModule());
//Get the first video stream
modules.add(new MapAllModule<>(probeResult.getVideoStreams()));
//Map all video streams
List<VideoStream> videoStreams = probeResult.getVideoStreams();
modules.add(new MapAllModule<>(videoStreams));
setOutputIndexes(videoStreams);
//Get the first audio stream in accordance with chosen languages
StreamSorter<AudioStream> audioSorter = new AudioLanguageSorter(this.audioLanguages)
.append(new ForcedFirstSorter<>(this.forcedAudioIndex))
.append(new SpecialAudioSorter(MinimalSubtitlePreference.REJECT));
List<AudioStream> sortedAudio = audioSorter.chainSort(probeResult.getAudioStreams());
setOutputIndexes(sortedAudio);
modules.add(new MapAllModule<>(sortedAudio));
modules.add(new SetDefaultStreamModule<>(sortedAudio, 0));
modules.add(new SetStreamLanguageModule<>(sortedAudio));
//Get the first subtitle stream in accordance with chosen languages and signs and songs prevention
StreamSorter<SubtitleStream> subtitleSorter = new SubtitleTitleSorter(
@ -109,13 +114,24 @@ public class AnimeConverter extends AbstractConverter {
.append(new SubtitleLanguageSorter(this.subtitleLanguages))
.append(new MinimalSubtitleSorter(this.subtitlePreference))
.append(new ForcedFirstSorter<>(this.forcedSubtitleIndex));
List<SubtitleStream> sorted = subtitleSorter.chainSort(probeResult.getSubtitleStreams());
modules.add(new MapAllModule<>(sorted));
modules.add(new SetDefaultStreamModule<>(sorted, 0));
List<SubtitleStream> sortedSubtitles = subtitleSorter.chainSort(probeResult.getSubtitleStreams());
setOutputIndexes(sortedSubtitles);
modules.add(new MapAllModule<>(sortedSubtitles));
modules.add(new SetDefaultStreamModule<>(sortedSubtitles, 0));
modules.add(new SetStreamLanguageModule<>(sortedSubtitles));
OutputUtil.printDebug("Subtitle preference: " + this.subtitleLanguages + ", Subtitle name filter: " +
this.subtitleNameFilter + ", Minimal Subtitle Preference: " + this.subtitlePreference.name() +
", Sorted order: " + Arrays.toString(sortedSubtitles.toArray()));
modules.add(new HardwareDecodeModule());
if (!this.forceAudioEncoding) {
modules.add(new CopyAudioModule());
if (configuration.useHardwareDecoding()) {
modules.add(new HardwareDecodeModule());
}
boolean encodeFlac = this.forceAudioEncoding || FFMpegConvert.getConfiguration().alwaysEncodeFlac();
for (AudioStream audioStream : sortedAudio) {
if (!encodeFlac || !audioStream.getCodecName().equalsIgnoreCase("flac")) {
modules.add(new CopyAudioModule(audioStream));
}
}
modules.add(new CopySubtitlesModule());
@ -129,10 +145,10 @@ public class AnimeConverter extends AbstractConverter {
if (encodingNecessary || this.forceVideoEncoding) {
List<String> availableHWAcceleration = getAvailableHardwareEncodingMethods();
if (FFMpegConvert.useHardwareAcceleration() && availableHWAcceleration.contains("qsv")) {
if (configuration.useHardwareEncoding() && availableHWAcceleration.contains("qsv")) {
modules.add(new SetVideoCodecModule("hevc_qsv"));
modules.add(new SetQualityModule(17, "veryslow"));
} else if (FFMpegConvert.useHardwareAcceleration() && availableHWAcceleration.contains("cuda")) {
} else if (configuration.useHardwareEncoding() && availableHWAcceleration.contains("cuda")) {
modules.add(new H265HardwareEncodingModule(20));
} else {
modules.add(new SetVideoCodecModule("hevc"));

View File

@ -12,6 +12,7 @@ import net.knarcraft.ffmpegconverter.converter.module.output.CopyAudioModule;
import net.knarcraft.ffmpegconverter.converter.module.output.CopySubtitlesModule;
import net.knarcraft.ffmpegconverter.converter.module.output.FastStartModule;
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
import net.knarcraft.ffmpegconverter.streams.AudioStream;
import net.knarcraft.ffmpegconverter.streams.StreamObject;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
import org.jetbrains.annotations.NotNull;
@ -64,9 +65,11 @@ public class MkvH264Converter extends AbstractConverter {
}
// Map audio if present
if (!probeResult.getAudioStreams().isEmpty()) {
modules.add(new MapAllModule<>(probeResult.getAudioStreams()));
modules.add(new CopyAudioModule());
List<AudioStream> audioStreams = probeResult.getAudioStreams();
if (!audioStreams.isEmpty()) {
modules.add(new MapAllModule<>(audioStreams));
setOutputIndexes(audioStreams);
modules.add(new CopyAudioModule(audioStreams));
}
// Map subtitles if present

View File

@ -69,7 +69,8 @@ public class MkvH265ReducedConverter extends AbstractConverter {
List<AudioStream> audioStreams = probeResult.getAudioStreams();
if (!audioStreams.isEmpty()) {
modules.add(new MapAllModule<>(audioStreams));
modules.add(new CopyAudioModule());
setOutputIndexes(audioStreams);
modules.add(new CopyAudioModule(audioStreams));
}
// Map subtitles if present

View File

@ -8,8 +8,10 @@ import net.knarcraft.ffmpegconverter.converter.module.ModuleExecutor;
import net.knarcraft.ffmpegconverter.converter.module.hardwarecoding.HardwareDecodeModule;
import net.knarcraft.ffmpegconverter.converter.module.mapping.MapAllModule;
import net.knarcraft.ffmpegconverter.converter.module.output.CopyAudioModule;
import net.knarcraft.ffmpegconverter.converter.module.output.CopyVideoModule;
import net.knarcraft.ffmpegconverter.converter.module.output.MovTextModule;
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
import net.knarcraft.ffmpegconverter.streams.AudioStream;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -53,8 +55,10 @@ public class SubtitleEmbed extends AbstractConverter {
modules.add(new MapAllModule<>(probeResult.parsedStreams()));
modules.add(new HardwareDecodeModule());
modules.add(new CopyAudioModule());
modules.add(new CopyAudioModule());
List<AudioStream> audioStreams = probeResult.getAudioStreams();
setOutputIndexes(audioStreams);
modules.add(new CopyAudioModule(audioStreams));
modules.add(new CopyVideoModule());
modules.add(new MovTextModule());
modules.add(new SetOutputFileModule(outFile));

View File

@ -1,5 +1,7 @@
package net.knarcraft.ffmpegconverter.converter;
import net.knarcraft.ffmpegconverter.FFMpegConvert;
import net.knarcraft.ffmpegconverter.config.Configuration;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
@ -33,8 +35,8 @@ import static net.knarcraft.ffmpegconverter.utility.FFMpegHelper.getNthSteam;
*/
public class WebAnimeConverter extends AbstractConverter {
private final String[] audioLanguages;
private final String[] subtitleLanguages;
private final List<String> audioLanguages;
private final List<String> subtitleLanguages;
private final boolean toStereo;
private final MinimalSubtitlePreference subtitlePreference;
private final int forcedAudioIndex;
@ -46,22 +48,20 @@ public class WebAnimeConverter extends AbstractConverter {
*
* @param ffprobePath <p>Path/command to ffprobe.</p>
* @param ffmpegPath <p>Path/command to ffmpeg.</p>
* @param audioLanguages <p>List of wanted audio languages in descending order.</p>
* @param subtitleLanguages <p>List of wanted subtitle languages in descending order.</p>
* @param toStereo <p>Convert video with several audio channels to stereo.</p>
* @param subtitlePreference <p>How minimal subtitles should be prioritized</p>
* @param forcedAudioIndex <p>A specific audio stream to force. 0-indexed from the first audio stream found</p>
* @param forcedSubtitleIndex <p>A specific subtitle stream to force. 0-indexed for the first subtitle stream found</p>
*/
public WebAnimeConverter(@NotNull String ffprobePath, @NotNull String ffmpegPath, @NotNull String[] audioLanguages,
@NotNull String[] subtitleLanguages, boolean toStereo,
public WebAnimeConverter(@NotNull String ffprobePath, @NotNull String ffmpegPath, boolean toStereo,
@NotNull MinimalSubtitlePreference subtitlePreference, int forcedAudioIndex,
int forcedSubtitleIndex, @NotNull String subtitleNameFilter) {
super("mp4");
Configuration configuration = FFMpegConvert.getConfiguration();
this.ffprobePath = ffprobePath;
this.ffmpegPath = ffmpegPath;
this.audioLanguages = audioLanguages;
this.subtitleLanguages = subtitleLanguages;
this.audioLanguages = configuration.getAnimeAudioLanguages();
this.subtitleLanguages = configuration.getAnimeSubtitleLanguages();
this.toStereo = toStereo;
this.subtitlePreference = subtitlePreference;
this.forcedAudioIndex = forcedAudioIndex;

View File

@ -2,16 +2,56 @@ package net.knarcraft.ffmpegconverter.converter.module.output;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
import net.knarcraft.ffmpegconverter.streams.AudioStream;
import net.knarcraft.ffmpegconverter.streams.StreamObject;
import org.jetbrains.annotations.NotNull;
import java.util.List;
/**
* A module for making FFMpeg copy the audio codec
*/
public class CopyAudioModule implements ConverterModule {
private final List<AudioStream> streams;
/**
* Instantiates a new copy audio module
*
* @param streams <p>The streams to specify the copy flag for, or null to not use a per-stream selector</p>
*/
public CopyAudioModule(@NotNull List<AudioStream> streams) {
this.streams = streams;
}
/**
* Instantiates a new copy audio module
*
* @param stream <p>The stream to specify the copy flag for</p>
*/
public CopyAudioModule(@NotNull AudioStream stream) {
this.streams = List.of(stream);
}
/**
* Instantiates a new copy audio module
*/
public CopyAudioModule() {
this.streams = null;
}
@Override
public void addArguments(@NotNull FFMpegCommand command) {
command.addOutputFileOption("-c:a", "copy");
if (this.streams != null) {
for (StreamObject streamObject : this.streams) {
int outputIndex = streamObject.getOutputIndex();
if (outputIndex != -1) {
command.addOutputFileOption("-c:a:" + outputIndex, "copy");
}
}
} else {
command.addOutputFileOption("-c:a", "copy");
}
}
}

View File

@ -0,0 +1,38 @@
package net.knarcraft.ffmpegconverter.converter.module.output;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
import net.knarcraft.ffmpegconverter.streams.StreamObject;
import org.jetbrains.annotations.NotNull;
import java.util.List;
/**
* A module for setting the language of some streams
*
* @param <K> <p>The type of stream to set language for</p>
*/
public class SetStreamLanguageModule<K extends StreamObject> implements ConverterModule {
private final List<K> streams;
/**
* Instantiates a new set stream language module
*
* @param streams <p>The streams to set language for</p>
*/
public SetStreamLanguageModule(@NotNull List<K> streams) {
this.streams = streams;
}
@Override
public void addArguments(@NotNull FFMpegCommand command) {
for (StreamObject stream : this.streams) {
if (!stream.getLanguage().equalsIgnoreCase("und") && !stream.getLanguage().isBlank()) {
command.addOutputFileOption(String.format("-metadata:s:%s:%d", stream.streamTypeCharacter(),
stream.getOutputIndex()), String.format("language=%s", stream.getLanguage()));
}
}
}
}

View File

@ -71,7 +71,7 @@ public abstract class AbstractSorter<L extends StreamObject> implements StreamSo
*/
@NotNull
protected <G extends StreamObject> List<G> sortStreamsByLanguage(@NotNull List<G> streams,
@NotNull String[] languages) {
@NotNull List<String> languages) {
List<G> sorted = new ArrayList<>();
for (String language : languages) {
for (G stream : streams) {

View File

@ -10,14 +10,14 @@ import java.util.List;
*/
public class AudioLanguageSorter extends AbstractSorter<AudioStream> {
private final String[] languageOrder;
private final List<String> languageOrder;
/**
* Instantiates a new audio language sorter
*
* @param languageOrder <p>The order of preference for audio languages</p>
*/
public AudioLanguageSorter(@NotNull String[] languageOrder) {
public AudioLanguageSorter(@NotNull List<String> languageOrder) {
this.languageOrder = languageOrder;
}

View File

@ -11,14 +11,14 @@ import java.util.List;
*/
public class SubtitleLanguageSorter extends AbstractSorter<SubtitleStream> {
private final String[] languageOrder;
private final List<String> languageOrder;
/**
* Instantiates a new subtitle language sorter
*
* @param languageOrder <p>The order of preference for subtitle languages</p>
*/
public SubtitleLanguageSorter(@NotNull String[] languageOrder) {
public SubtitleLanguageSorter(@NotNull List<String> languageOrder) {
this.languageOrder = languageOrder;
}

View File

@ -45,7 +45,7 @@ public class SubtitleTitleSorter extends AbstractSorter<SubtitleStream> {
boolean isRegEx = isValidRegularExpression(filter) && hasSpecialRegexCharacters(filter);
if (FFMpegConvert.isDebugEnabled()) {
if (FFMpegConvert.getConfiguration().isDebugEnabled()) {
System.out.println("Filtering subtitles by filter " + filter + ". RegEx is " + isRegEx);
}

View File

@ -4,7 +4,6 @@ import net.knarcraft.ffmpegconverter.FFMpegConvert;
import net.knarcraft.ffmpegconverter.config.ConfigHandler;
import net.knarcraft.ffmpegconverter.config.ConfigKey;
import net.knarcraft.ffmpegconverter.utility.OutputUtil;
import org.apache.commons.configuration2.Configuration;
import org.apache.commons.configuration2.PropertiesConfiguration;
import org.jetbrains.annotations.NotNull;
@ -17,7 +16,7 @@ import java.util.List;
*/
public record AvailableHardwareEncoderHandler(@NotNull List<String> availableHardwareEncodings) {
private static final ConfigHandler configHandler = FFMpegConvert.getConfigHandler();
private static final ConfigHandler configHandler = FFMpegConvert.getConfiguration().getConfigHandler();
/**
* Gets all hardware encodings
@ -44,7 +43,7 @@ public record AvailableHardwareEncoderHandler(@NotNull List<String> availableHar
*/
public void save() {
PropertiesConfiguration configuration = configHandler.getWritableConfiguration();
configuration.setProperty(ConfigKey.HARDWARE_ACCELERATED_ENCODERS.toString(), this.availableHardwareEncodings);
configuration.setProperty(ConfigKey.HARDWARE_ACCELERATED_ENCODERS.toString(), String.join(",", this.availableHardwareEncodings));
configHandler.writeConfiguration();
OutputUtil.printDebug("Saved available hardware encoder handler");
}
@ -56,14 +55,12 @@ public record AvailableHardwareEncoderHandler(@NotNull List<String> availableHar
*/
@NotNull
public static AvailableHardwareEncoderHandler load() {
Configuration configuration;
try {
configuration = configHandler.load();
configHandler.load();
} catch (IOException e) {
throw new RuntimeException(e);
}
List<String> getEncodings = configuration.getList(String.class, ConfigKey.HARDWARE_ACCELERATED_ENCODERS.toString());
return new AvailableHardwareEncoderHandler(getEncodings);
return new AvailableHardwareEncoderHandler(FFMpegConvert.getConfiguration().hardwareEncoders());
}
}

View File

@ -17,6 +17,7 @@ public abstract class AbstractStream implements StreamObject {
protected final String language;
protected final boolean isDefault;
protected final String title;
protected int outputIndex;
/**
* Instantiates a new abstract stream
@ -28,11 +29,12 @@ public abstract class AbstractStream implements StreamObject {
protected AbstractStream(@NotNull Map<StreamTag, String> streamInfo, int inputIndex, int relativeIndex) {
this.codecName = ValueParsingHelper.parseString(streamInfo.get(StreamTag.CODEC_NAME), "");
this.absoluteIndex = ValueParsingHelper.parseInt(streamInfo.get(StreamTag.INDEX), -1);
this.language = ValueParsingHelper.parseString(streamInfo.get(StreamTag.TAG_LANGUAGE), "und");
this.language = parseLanguage(streamInfo);
this.isDefault = ValueParsingHelper.parseBoolean(streamInfo.get(StreamTag.DISPOSITION_DEFAULT), false);
this.title = ValueParsingHelper.parseString(streamInfo.get(StreamTag.TAG_TITLE), "");
this.inputIndex = inputIndex;
this.relativeIndex = relativeIndex;
this.outputIndex = -1;
}
@Override
@ -73,4 +75,40 @@ public abstract class AbstractStream implements StreamObject {
return this.inputIndex;
}
@Override
public int getOutputIndex() {
return this.outputIndex;
}
@Override
public void setOutputIndex(int newIndex) {
this.outputIndex = newIndex;
}
/**
* Parses the correct language of a stream
*
* <p>As some people tend to set weird incorrect languages for anime streams, this tries to correct that by
* detecting streams that are actually in english, but set to something else.</p>
*
* @param streamInfo <p>All info about the stream</p>
* @return <p>The actual language</p>
*/
@NotNull
private String parseLanguage(@NotNull Map<StreamTag, String> streamInfo) {
String languageString = ValueParsingHelper.parseString(streamInfo.get(StreamTag.TAG_LANGUAGE), "und");
String title = ValueParsingHelper.parseString(streamInfo.get(StreamTag.TAG_TITLE), "");
if (languageString.equalsIgnoreCase("zxx") ||
(title.toLowerCase().matches(".*english.*") && languageString.equalsIgnoreCase("jpn"))) {
return "eng";
}
return languageString;
}
@Override
@NotNull
public String toString() {
return this.title + " | Input index: " + inputIndex + " | Language: " + language;
}
}

View File

@ -5,6 +5,7 @@ import org.jetbrains.annotations.NotNull;
/**
* An object describing a generic video file stream
*/
@SuppressWarnings("unused")
public interface StreamObject {
/**
@ -68,4 +69,21 @@ public interface StreamObject {
*/
char streamTypeCharacter();
/**
* Gets the index of this stream in the output file
*
* @return <p>The index of this stream in the output file, or -1 if not set</p>
*/
int getOutputIndex();
/**
* Sets the index of this stream in the output file
*
* <p>After sorting and selecting streams, setting the output index makes it much easier to set properties for this
* specific stream, without having to rely on loops</p>
*
* @param newIndex <p>The new output index</p>
*/
void setOutputIndex(int newIndex);
}

View File

@ -51,7 +51,7 @@ public class SubtitleStream extends AbstractStream implements StreamObject {
*/
private boolean checkIfIsFullSubtitle() {
String titleLowercase = getTitle().toLowerCase().trim();
return !titleLowercase.matches(".*si(ng|gn)s?[ &/a-z]+songs?.*") &&
return !titleLowercase.matches(".*si(ng|gn)s?[ &/a-z+]+songs?.*") &&
!titleLowercase.matches(".*songs?[ &/a-z]+si(gn|ng)s?.*") &&
!titleLowercase.matches(".*forced.*") &&
!titleLowercase.matches(".*s&s.*") &&
@ -68,4 +68,10 @@ public class SubtitleStream extends AbstractStream implements StreamObject {
return 's';
}
@Override
@NotNull
public String toString() {
return super.toString() + " | Is full: " + this.isFullSubtitle;
}
}

View File

@ -0,0 +1,60 @@
package net.knarcraft.ffmpegconverter.utility;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* A helper class for dealing with configuration value types
*/
public final class ConfigHelper {
private ConfigHelper() {
}
/**
* Gets the given value as a string list
*
* @param value <p>The raw string list value</p>
* @return <p>The value as a string list, or null if not compatible</p>
*/
public static @NotNull List<String> asStringList(@Nullable Object value) {
if (value == null) {
return new ArrayList<>();
}
if (value instanceof String string) {
return List.of((string).split(","));
} else if (value instanceof List<?> list) {
List<String> strings = new ArrayList<>();
for (Object object : list) {
strings.add(String.valueOf(object));
}
return strings;
} else {
return new ArrayList<>();
}
}
/**
* Gets the given value as a boolean
*
* <p>This will throw an exception if used for a non-boolean value</p>
*
* @param value <p>The object value to get</p>
* @return <p>The value of the given object as a boolean</p>
* @throws ClassCastException <p>If the given value is not a boolean</p>
*/
public static boolean asBoolean(@Nullable Object value) throws ClassCastException {
if (value instanceof Boolean booleanValue) {
return booleanValue;
} else if (value instanceof String) {
return Boolean.parseBoolean((String) value);
} else {
throw new ClassCastException();
}
}
}

View File

@ -3,4 +3,16 @@
debug=false
# Enabling hardware acceleration will try to use hardware acceleration for converters where it's available. Note that
# software encoders generally produce a lower file-size relative to the output quality.
hardware-acceleration=false
hardware-acceleration-encode=false
# Hardware decoding can often speed up the conversion, but might be troublesome at times
hardware-acceleration-decode=true
# The available hardware encoders
encoders-hardware-accelerated=qsv,cuda,vaapi,dxva2,d3d11va,opencl,vulkan,d3d12va
# As FLAC can increase file size significantly, this option enabled automatic re-encode of flac tracks
encode-flac-always=false
# The preference for audio languages when converting anime (0 = undefined, * = any)
audio-languages-anime=jpn,eng,*
# The preference for subtitle languages when converting anime (0 = undefined, * = any)
subtitle-languages-anime=eng,*
# The preference for minimal subtitles, AKA Signs & Songs (REQUIRE/PREFER/NO_PREFERENCE/AVOID/REJECT)
minimal-subtitle-preference=AVOID